feat: move register flow to RSC
This commit is contained in:
parent
2e8c8883c5
commit
93dc514a2b
12 changed files with 80 additions and 151 deletions
|
@ -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} />;
|
||||
};
|
|
@ -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;
|
22
src/app/(auth)/register/page.tsx
Normal file
22
src/app/(auth)/register/page.tsx
Normal 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 />;
|
||||
}
|
32
src/app/actions/register/register-action.ts
Normal file
32
src/app/actions/register/register-action.ts
Normal 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);
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import React from 'react';
|
||||
import { RegisterContainer } from '../../containers/RegisterContainer';
|
||||
|
||||
export const RegisterPage = () => {
|
||||
return <RegisterContainer />;
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { RegisterPage } from './RegisterPage';
|
|
@ -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: {},
|
||||
});
|
||||
};
|
Loading…
Add table
Reference in a new issue