From 51df2a94e938116f719ac812483323f5ffb116a1 Mon Sep 17 00:00:00 2001 From: Jackson Tran <69227799+javtran@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:14:17 -0700 Subject: [PATCH] [Closes #95] User can register through invite (#99) * user can accept invite and register * backend changes for register without license * Fixes and API tweaks * Fix dependency array again * license field hides when invite role is not first_responder --------- Co-authored-by: Francis Li --- client/src/App.jsx | 1 + client/src/pages/register/RegisterForm.jsx | 8 +++- client/src/pages/register/register.jsx | 48 +++++++++++++++++++++- server/models/user.js | 4 ++ server/routes/api/v1/users/register.js | 48 +++++++++++++--------- server/test/routes/api/v1/users.test.js | 2 +- 6 files changed, 88 insertions(+), 23 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 64d34a46..89331311 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -104,6 +104,7 @@ function App() { element={} > } /> + } /> } /> } /> diff --git a/client/src/pages/register/RegisterForm.jsx b/client/src/pages/register/RegisterForm.jsx index dfd783f2..51ef210e 100644 --- a/client/src/pages/register/RegisterForm.jsx +++ b/client/src/pages/register/RegisterForm.jsx @@ -18,6 +18,7 @@ const registerFormProps = { onSubmit: PropTypes.func.isRequired, setShowLicenseHelper: PropTypes.func.isRequired, formState: PropTypes.number.isRequired, + showLicenseField: PropTypes.string.isRequired, }; /** @@ -33,6 +34,7 @@ export function RegisterForm({ onSubmit, setShowLicenseHelper, formState, + showLicenseField, }) { return ( <> @@ -43,7 +45,7 @@ export function RegisterForm({ }} > - {formState !== 3 && ( + {formState !== 3 && showLicenseField && ( <> {isLoading ? ( diff --git a/client/src/pages/register/register.jsx b/client/src/pages/register/register.jsx index dd0b94b8..4c661ccd 100644 --- a/client/src/pages/register/register.jsx +++ b/client/src/pages/register/register.jsx @@ -1,12 +1,16 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import classes from './register.module.css'; import { RegisterForm } from './RegisterForm'; import { Flex } from '@mantine/core'; +import { useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { notifications } from '@mantine/notifications'; /** * Register page component */ function Register() { + const { inviteId } = useParams(); const [user, setUser] = useState({ firstName: '', middleName: '', @@ -19,6 +23,45 @@ function Register() { const [showLicenseHelper, setShowLicenseHelper] = useState(false); const [isLoading, setIsLoading] = useState(false); const [formState, setFormState] = useState(1); + const navigate = useNavigate(); + + useEffect(() => { + if (!inviteId) return; + fetch(`/api/v1/invites/${inviteId}`) + .then((response) => { + if (!response.ok) { + return Promise.reject(response); + } + return response.json(); + }) + .then((data) => { + if (data.acceptedById.length != 0) { + throw Error(); + } + + setUser((prevUser) => ({ + ...prevUser, + firstName: data.firstName, + middleName: data.middleName, + lastName: data.lastName, + email: data.email, + role: data.role, + inviteId: inviteId, + })); + + if (data.role != 'FIRST_RESPONDER') { + setFormState(2); + } + }) + .catch(() => { + notifications.show({ + color: 'red', + title: `Invalid invite`, + autoClose: 5000, + }); + navigate('/register'); + }); + }, [inviteId, navigate]); /** * Handles input fields in the Registration form @@ -137,6 +180,9 @@ function Register() { setShowLicenseHelper(!showLicenseHelper); }} formState={formState} + showLicenseField={ + !inviteId || (inviteId && user.role == 'FIRST_RESPONDER') + } /> diff --git a/server/models/user.js b/server/models/user.js index 41a325c1..6a3cd870 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -38,6 +38,10 @@ class User extends Base { inviteId: z.string().optional(), }); + static get Role() { + return Role; + } + constructor(data) { super(Prisma.UserScalarFieldEnum, data); } diff --git a/server/routes/api/v1/users/register.js b/server/routes/api/v1/users/register.js index fdd28a35..e6594312 100644 --- a/server/routes/api/v1/users/register.js +++ b/server/routes/api/v1/users/register.js @@ -101,26 +101,31 @@ export default async function (fastify, _opts) { let licenseData; // Validate License Numbers - try { - const licenseResponse = await verifyLicense(licenseNumber); - if (licenseResponse && licenseResponse.status != 'Expired') { - const userFromLicense = await fastify.prisma.user.findUnique({ - where: { licenseNumber: licenseNumber }, - }); + if ( + (!invite && licenseNumber) || + invite.role == User.Role.FIRST_RESPONDER + ) { + try { + const licenseResponse = await verifyLicense(licenseNumber); + if (licenseResponse && licenseResponse.status !== 'Expired') { + const userFromLicense = await fastify.prisma.user.findUnique({ + where: { licenseNumber: licenseNumber }, + }); - if (userFromLicense) { - throw new Error('License already registered'); - } + if (userFromLicense) { + throw new Error('License already registered'); + } - licenseData = licenseResponse; - } else { - throw new Error('Expired or unprocessable license data'); + licenseData = licenseResponse; + } else { + throw new Error('Expired or unprocessable license data'); + } + } catch (error) { + errorList.push({ + path: 'licenseNumber', + message: error.message, + }); } - } catch (error) { - errorList.push({ - path: 'licenseNumber', - message: error.message, - }); } if (errorList.length) { @@ -142,13 +147,18 @@ export default async function (fastify, _opts) { // Hash the password await user.setPassword(password); + const now = new Date().toISOString(); if (invite) { user.role = invite.role; + if (user.role !== User.Role.FIRST_RESPONDER) { + user.licenseNumber = null; + } user.approvedById = invite.invitedById; user.approvedAt = invite.createdAt; + user.emailVerifiedAt = now; } else { // Set role - user.role = 'FIRST_RESPONDER'; + user.role = User.Role.FIRST_RESPONDER; // Generate verification token and send user.generateEmailVerificationToken(); await user.sendVerificationEmail(); @@ -161,7 +171,7 @@ export default async function (fastify, _opts) { where: { id: invite.id }, data: { acceptedById: data.id, - acceptedAt: new Date().toISOString(), + acceptedAt: now, }, }); } diff --git a/server/test/routes/api/v1/users.test.js b/server/test/routes/api/v1/users.test.js index 6bdb7e39..c27c5395 100644 --- a/server/test/routes/api/v1/users.test.js +++ b/server/test/routes/api/v1/users.test.js @@ -220,7 +220,6 @@ describe('/api/v1/users', () => { lastName: 'Doe', email: 'john.doe@test.com', password: 'Test123!', - licenseNumber: 'P39332', inviteId: '6ed61e21-1062-4b10-a967-53b395f5c34c', }); assert.deepStrictEqual(res.statusCode, StatusCodes.CREATED); @@ -241,6 +240,7 @@ describe('/api/v1/users', () => { Date.parse(user.approvedAt), Date.parse('2024-04-07T16:53:41-07:00'), ); + assert.ok(user.emailVerifiedAt); const invite = await t.prisma.invite.findUnique({ where: { id: '6ed61e21-1062-4b10-a967-53b395f5c34c' },