feat: move register flow to RSC

This commit is contained in:
Nicolas Meienberger 2023-09-18 11:05:08 -07:00 committed by Nicolas Meienberger
parent 2e8c8883c5
commit 93dc514a2b
12 changed files with 80 additions and 151 deletions

View file

@ -0,0 +1,24 @@
'use client';
import React from 'react';
import { useAction } from 'next-safe-action/hook';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { registerAction } from '@/actions/register/register-action';
import { RegisterForm } from '../RegisterForm';
export const RegisterContainer: React.FC = () => {
const router = useRouter();
const registerMutation = useAction(registerAction, {
onSuccess: (data) => {
if (!data.success) {
toast.error(data.failure.reason);
} else {
router.push('/dashboard');
}
},
});
return <RegisterForm onSubmit={({ email, password }) => registerMutation.execute({ username: email, password })} loading={registerMutation.isExecuting} />;
};

View file

@ -3,8 +3,8 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useTranslations } from 'next-intl';
import { Button } from '../../../../components/ui/Button';
import { Input } from '../../../../components/ui/Input';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
interface IProps {
onSubmit: (values: FormValues) => void;

View file

@ -0,0 +1,22 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { getUserFromCookie } from '@/server/common/session.helpers';
import { AuthQueries } from '@/server/queries/auth/auth.queries';
import { db } from '@/server/db';
import { RegisterContainer } from './components/RegisterContainer';
export default async function LoginPage() {
const user = await getUserFromCookie();
if (user) {
redirect('/dashboard');
}
const authQueries = new AuthQueries(db);
const isConfigured = await authQueries.getFirstOperator();
if (isConfigured) {
redirect('/login');
}
return <RegisterContainer />;
}

View file

@ -0,0 +1,32 @@
'use server';
import { z } from 'zod';
import { db } from '@/server/db';
import { AuthServiceClass } from '@/server/services/auth/auth.service';
import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache';
import { handleActionError } from '../utils/handle-action-error';
const input = z.object({
username: z.string(),
password: z.string(),
});
/**
* Given a username and password, registers the user and logs them in.
*/
export const registerAction = action(input, async ({ username, password }) => {
try {
const authService = new AuthServiceClass(db);
const result = await authService.register({ username, password });
if (result) {
revalidatePath('/register');
}
return { success: true };
} catch (e) {
return handleActionError(e);
}
});

View file

@ -1,81 +0,0 @@
import { faker } from '@faker-js/faker';
import React from 'react';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { RegisterContainer } from './RegisterContainer';
const pushFn = jest.fn();
jest.mock('next/router', () => {
const actualRouter = jest.requireActual('next-router-mock');
return {
...actualRouter,
useRouter: () => ({
...actualRouter.useRouter(),
push: pushFn,
}),
};
});
beforeEach(() => {
pushFn.mockClear();
});
describe('Test: RegisterContainer', () => {
it('should render without error', () => {
render(<RegisterContainer />);
expect(screen.getByText('Register')).toBeInTheDocument();
});
it('should redirect to / upon successful registration', async () => {
// Arrange
const email = faker.internet.email();
const password = faker.internet.password();
server.use(getTRPCMock({ path: ['auth', 'register'], type: 'mutation', response: true, delay: 100 }));
render(<RegisterContainer />);
// Act
const registerButton = screen.getByRole('button', { name: 'Register' });
const emailInput = screen.getByLabelText('Email address');
const passwordInput = screen.getByLabelText('Password');
const confirmPasswordInput = screen.getByLabelText('Confirm password');
fireEvent.change(emailInput, { target: { value: email } });
fireEvent.change(passwordInput, { target: { value: password } });
fireEvent.change(confirmPasswordInput, { target: { value: password } });
fireEvent.click(registerButton);
// Assert
await waitFor(() => {
expect(pushFn).toHaveBeenCalledWith('/');
});
});
it('should show toast if register mutation fails', async () => {
// Arrange
const email = faker.internet.email();
const password = faker.internet.password();
server.use(getTRPCMockError({ path: ['auth', 'register'], type: 'mutation', status: 500, message: 'my big error' }));
render(<RegisterContainer />);
// Act
const registerButton = screen.getByRole('button', { name: 'Register' });
const emailInput = screen.getByLabelText('Email address');
const passwordInput = screen.getByLabelText('Password');
const confirmPasswordInput = screen.getByLabelText('Confirm password');
fireEvent.change(emailInput, { target: { value: email } });
fireEvent.change(passwordInput, { target: { value: password } });
fireEvent.change(confirmPasswordInput, { target: { value: password } });
fireEvent.click(registerButton);
// Assert
await waitFor(() => {
expect(screen.getByText('my big error')).toBeInTheDocument();
});
});
});

View file

@ -1,35 +0,0 @@
import { useRouter } from 'next/router';
import React from 'react';
import { toast } from 'react-hot-toast';
import { useLocale } from '@/client/hooks/useLocale';
import { useTranslations } from 'next-intl';
import type { MessageKey } from '@/server/utils/errors';
import { trpc } from '../../../../utils/trpc';
import { AuthFormLayout } from '../../components/AuthFormLayout';
import { RegisterForm } from '../../components/RegisterForm';
type FormValues = { email: string; password: string };
export const RegisterContainer: React.FC = () => {
const t = useTranslations();
const { locale } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const register = trpc.auth.register.useMutation({
onError: (e) => toast.error(t(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables })),
onSuccess: () => {
utils.auth.me.invalidate();
router.push('/');
},
});
const handlerSubmit = (value: FormValues) => {
register.mutate({ username: value.email, password: value.password, locale });
};
return (
<AuthFormLayout>
<RegisterForm onSubmit={handlerSubmit} loading={register.isLoading} />
</AuthFormLayout>
);
};

View file

@ -1,13 +0,0 @@
import React from 'react';
import { render, waitFor, screen } from '../../../../../../tests/test-utils';
import { RegisterPage } from './RegisterPage';
describe('Test: RegisterPage', () => {
it('should render correctly', async () => {
render(<RegisterPage />);
await waitFor(() => {
expect(screen.getByText('Register')).toBeInTheDocument();
});
});
});

View file

@ -1,6 +0,0 @@
import React from 'react';
import { RegisterContainer } from '../../containers/RegisterContainer';
export const RegisterPage = () => {
return <RegisterContainer />;
};

View file

@ -1 +0,0 @@
export { RegisterPage } from './RegisterPage';

View file

@ -1,13 +0,0 @@
import { getMessagesPageProps } from '@/utils/page-helpers';
import merge from 'lodash.merge';
import { GetServerSideProps } from 'next';
export { RegisterPage as default } from '../client/modules/Auth/pages/RegisterPage';
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const messagesProps = await getMessagesPageProps(ctx);
return merge(messagesProps, {
props: {},
});
};