Skip to content

Commit

Permalink
[Closes #95] User can register through invite (#99)
Browse files Browse the repository at this point in the history
* 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 <mail@francisli.com>
  • Loading branch information
javtran and francisli authored Aug 26, 2024
1 parent 35ace79 commit 51df2a9
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 23 deletions.
1 change: 1 addition & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ function App() {
element={<Redirect isLoading={isLoading} isLoggedIn={isLoggedIn} />}
>
<Route path="/register" element={<Register />} />
<Route path="/register/:inviteId" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="/" element={<Index />} />
</Route>
Expand Down
8 changes: 6 additions & 2 deletions client/src/pages/register/RegisterForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const registerFormProps = {
onSubmit: PropTypes.func.isRequired,
setShowLicenseHelper: PropTypes.func.isRequired,
formState: PropTypes.number.isRequired,
showLicenseField: PropTypes.string.isRequired,
};

/**
Expand All @@ -33,6 +34,7 @@ export function RegisterForm({
onSubmit,
setShowLicenseHelper,
formState,
showLicenseField,
}) {
return (
<>
Expand All @@ -43,7 +45,7 @@ export function RegisterForm({
}}
>
<Container size="25rem" className={classes.form}>
{formState !== 3 && (
{formState !== 3 && showLicenseField && (
<>
<TextInput
disabled={isLoading || formState === 2}
Expand Down Expand Up @@ -142,7 +144,9 @@ export function RegisterForm({
{formState !== 3 && (
<Button
type="submit"
disabled={!user.licenseNumber.length || isLoading}
disabled={
(!user.licenseNumber.length && !user.inviteId) || isLoading
}
>
{isLoading ? (
<Loader size={20} />
Expand Down
48 changes: 47 additions & 1 deletion client/src/pages/register/register.jsx
Original file line number Diff line number Diff line change
@@ -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: '',
Expand All @@ -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
Expand Down Expand Up @@ -137,6 +180,9 @@ function Register() {
setShowLicenseHelper(!showLicenseHelper);
}}
formState={formState}
showLicenseField={
!inviteId || (inviteId && user.role == 'FIRST_RESPONDER')
}
/>
</Flex>
</div>
Expand Down
4 changes: 4 additions & 0 deletions server/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class User extends Base {
inviteId: z.string().optional(),
});

static get Role() {
return Role;
}

constructor(data) {
super(Prisma.UserScalarFieldEnum, data);
}
Expand Down
48 changes: 29 additions & 19 deletions server/routes/api/v1/users/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
Expand All @@ -161,7 +171,7 @@ export default async function (fastify, _opts) {
where: { id: invite.id },
data: {
acceptedById: data.id,
acceptedAt: new Date().toISOString(),
acceptedAt: now,
},
});
}
Expand Down
2 changes: 1 addition & 1 deletion server/test/routes/api/v1/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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' },
Expand Down

0 comments on commit 51df2a9

Please sign in to comment.