diff --git a/packages/api/src/api/project/contributors/put.ts b/packages/api/src/api/project/contributors/put.ts index e3cd858c2..ec7098b62 100644 --- a/packages/api/src/api/project/contributors/put.ts +++ b/packages/api/src/api/project/contributors/put.ts @@ -15,8 +15,10 @@ export const put = async (req: Request, res: Response) => { }); if (errorHandled) return; - const project = await entityManager.findOneOrFail(Project, { id: data.projectId }); - if (project.inviteCode !== data.inviteCode) { + let project: Project; + try { + project = await entityManager.findOneOrFail(Project, { inviteCode: data.inviteCode }); + } catch { res.sendStatus(404); return; } diff --git a/packages/api/tests/api/project/contributors/put.test.ts b/packages/api/tests/api/project/contributors/put.test.ts index a55ece8a5..953abfd4f 100644 --- a/packages/api/tests/api/project/contributors/put.test.ts +++ b/packages/api/tests/api/project/contributors/put.test.ts @@ -7,7 +7,7 @@ import { validatePayload } from '../../../../src/utils/validatePayload'; jest.mock('../../../../src/utils/validatePayload'); const validatePayloadMock = getMock(validatePayload); -const validPayload = { projectId: '1', inviteCode: '00000000-0000-0000-0000-000000000000' }; +const validPayload = { inviteCode: '00000000-0000-0000-0000-000000000000' }; const mockUser = { id: '1' }; describe('project contributors put endpoint', () => { @@ -27,21 +27,6 @@ describe('project contributors put endpoint', () => { expect(res.send).toHaveBeenCalledWith(mockProject); }); - it('should return 404 if the invite code is invalid', async () => { - validatePayloadMock.mockReturnValueOnce({ errorHandled: false, data: validPayload } as any); - const req = createMockRequest({ user: mockUser, ...validPayload } as any); - const res = createMockResponse(); - const mockProject = { inviteCode: 'invalid', contributors: { add: jest.fn() } }; - req.entityManager.findOneOrFail.mockResolvedValueOnce(mockProject); - - await put(req as any, res as any); - - expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId }); - expect(mockProject.contributors.add).not.toBeCalled(); - expect(req.entityManager.persist).not.toBeCalled(); - expect(res.sendStatus).toHaveBeenCalledWith(404); - }); - it('should return 409 if the user is already a contributor', async () => { validatePayloadMock.mockReturnValueOnce({ errorHandled: false, data: validPayload } as any); const req = createMockRequest({ user: mockUser, ...validPayload } as any); @@ -52,7 +37,9 @@ describe('project contributors put endpoint', () => { await put(req as any, res as any); - expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId }); + expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { + inviteCode: validPayload.inviteCode, + }); expect(mockProject.contributors.add).not.toBeCalled(); expect(req.entityManager.persist).not.toBeCalled(); expect(res.sendStatus).toHaveBeenCalledWith(409); @@ -68,7 +55,9 @@ describe('project contributors put endpoint', () => { await put(req as any, res as any); - expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId }); + expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { + inviteCode: validPayload.inviteCode, + }); expect(mockProject.contributors.add).not.toBeCalled(); expect(req.entityManager.persist).not.toBeCalled(); expect(res.sendStatus).toHaveBeenCalledWith(423); @@ -84,7 +73,9 @@ describe('project contributors put endpoint', () => { await put(req as any, res as any); - expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId }); + expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { + inviteCode: validPayload.inviteCode, + }); expect(mockProject.contributors.add).not.toBeCalled(); expect(req.entityManager.persist).not.toBeCalled(); expect(res.sendStatus).toHaveBeenCalledWith(500); @@ -105,15 +96,13 @@ describe('project contributors put endpoint', () => { validatePayloadMock.mockReturnValueOnce({ errorHandled: false, data: validPayload } as any); const req = createMockRequest({ user: mockUser, ...validPayload } as any); const res = createMockResponse(); - req.entityManager.findOneOrFail.mockResolvedValueOnce({ - ...validPayload, - inviteCode: '2', - } as any); req.entityManager.findOneOrFail.mockRejectedValueOnce({ name: 'NotFoundError' }); await put(req as any, res as any); - expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { id: validPayload.projectId }); + expect(req.entityManager.findOneOrFail).toBeCalledWith(Project, { + inviteCode: validPayload.inviteCode, + }); expect(res.sendStatus).toHaveBeenCalledWith(404); }); }); diff --git a/packages/shared/src/schema/project/contributors/put.ts b/packages/shared/src/schema/project/contributors/put.ts index 3f51a784b..6928e20af 100644 --- a/packages/shared/src/schema/project/contributors/put.ts +++ b/packages/shared/src/schema/project/contributors/put.ts @@ -1,6 +1,5 @@ import { z } from 'zod'; export const put = z.object({ - projectId: z.string(), inviteCode: z.string().uuid(), }); diff --git a/packages/shared/tests/schema/project/contributors/put.test.ts b/packages/shared/tests/schema/project/contributors/put.test.ts index 63ba767cf..061311682 100644 --- a/packages/shared/tests/schema/project/contributors/put.test.ts +++ b/packages/shared/tests/schema/project/contributors/put.test.ts @@ -11,15 +11,6 @@ describe('project contributors put schema', () => { expect(Schema.project.contributors.put.safeParse(validPut).success).toBe(true); }); - it('does not validate an object with an invalid projectId', () => { - expect( - Schema.project.contributors.put.safeParse({ - ...validPut, - projectId: 1, - }).success, - ).toBe(false); - }); - it('does not validate an object with an invalid inviteCode', () => { expect( Schema.project.contributors.put.safeParse({ diff --git a/packages/web/src/components/useProjectInviteCodeHandler/index.ts b/packages/web/src/components/useProjectInviteCodeHandler/index.ts new file mode 100644 index 000000000..18624042b --- /dev/null +++ b/packages/web/src/components/useProjectInviteCodeHandler/index.ts @@ -0,0 +1 @@ +export * from './useProjectInviteCodeHandler'; diff --git a/packages/web/src/components/useProjectInviteCodeHandler/useProjectInviteCodeHandler.tsx b/packages/web/src/components/useProjectInviteCodeHandler/useProjectInviteCodeHandler.tsx new file mode 100644 index 000000000..ceb690581 --- /dev/null +++ b/packages/web/src/components/useProjectInviteCodeHandler/useProjectInviteCodeHandler.tsx @@ -0,0 +1,60 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import axios from 'axios'; +import { useUserStore } from '../../stores/user'; +import { openErrorToast, openSuccessToast } from '../utils/CustomToast'; +import { useRedirectToAuth } from '../layout/RedirectToAuthModal'; + +const toasts = { + Success: () => + openSuccessToast({ + title: `Registration successful!`, + description: "You're all set! 🚀", + }), + Invalid: () => + openErrorToast({ + title: 'Invalid invite code', + description: 'Please check your invite code and try again.', + }), + ProjectExists: () => + openErrorToast({ + title: 'Project already exists', + description: 'You already have a project.', + }), +}; + +const useProjectInviteCodeHandler = () => { + const router = useRouter(); + const { triggerRedirect } = useRedirectToAuth(); + const { user, doneLoading: userLoaded } = useUserStore(); + const { projectInviteCode } = router.query; + + useEffect(() => { + if (!userLoaded || !projectInviteCode) return; + if (!user) { + triggerRedirect({ returnTo: `/?projectInviteCode=${projectInviteCode}` }); + return; + } + if (user?.project) { + toasts.ProjectExists(); + return; + } + + void (async () => { + let project; + try { + project = (await axios.put(`/api/project/contributors`, { inviteCode: projectInviteCode })) + .data; + } catch { + toasts.Invalid(); + return; + } + toasts.Success(); + + await useUserStore.getState().fetchUser(); + void router.push(`/project/${project.id}`); + })(); + }, [projectInviteCode, userLoaded, triggerRedirect]); // eslint-disable-line react-hooks/exhaustive-deps +}; + +export { useProjectInviteCodeHandler }; diff --git a/packages/web/src/pages/index.tsx b/packages/web/src/pages/index.tsx index 2149b49d0..5ab154e75 100644 --- a/packages/web/src/pages/index.tsx +++ b/packages/web/src/pages/index.tsx @@ -6,8 +6,10 @@ import { PageContainer } from '../components/layout/PageContainer'; import { Prizes } from '../components/Prizes'; import { usePrizesStore } from '../stores/prizes'; import { ProjectRegistrationCTA } from '../components/ProjectRegistrationCTA'; +import { useProjectInviteCodeHandler } from '../components/useProjectInviteCodeHandler'; const Home: NextPage = () => { + useProjectInviteCodeHandler(); const { doneLoading: prizesFetched } = usePrizesStore(); React.useEffect(() => { const { prizes } = usePrizesStore.getState();