From 51802492b1ff7b0469b2737178abcfef77d2fa2b Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Fri, 25 Oct 2024 10:10:26 +0200 Subject: [PATCH] chore(api): add ExamEnvironmentAuthorizationToken -> user relation (#56627) Co-authored-by: Oliver Eyton-Williams --- api/jest.utils.ts | 5 ++ api/prisma/schema.prisma | 11 ++- .../routes/exam-environment.ts | 2 +- api/src/routes/protected/user.test.ts | 72 +++++++++++++++++++ 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/api/jest.utils.ts b/api/jest.utils.ts index 2afbaba0115..174ac5c8f4d 100644 --- a/api/jest.utils.ts +++ b/api/jest.utils.ts @@ -207,6 +207,11 @@ export const defaultUserEmail = 'foo@bar.com'; export const defaultUsername = 'fcc-test-user'; export const resetDefaultUser = async (): Promise => { + await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany( + { + where: { userId: defaultUserId } + } + ); await fastifyTestInstance.prisma.user.deleteMany({ where: { email: defaultUserEmail } }); diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index e9022dcd226..cec7cf4c82c 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -143,7 +143,8 @@ model user { isClassroomAccount Boolean? // Undefined // Relations - examAttempts EnvExamAttempt[] + examAttempts EnvExamAttempt[] + examEnvironmentAuthorizationToken ExamEnvironmentAuthorizationToken? } // ----------------------------------- @@ -377,15 +378,13 @@ model UserToken { @@index([userId], map: "userId_1") } -/// TODO: Token has to outlive the exam attempt -/// Validation has to be taken as the attempt is requested -/// to ensure it lives long enough. model ExamEnvironmentAuthorizationToken { id String @id @map("_id") createdDate DateTime @db.Date - userId String @db.ObjectId + userId String @unique @db.ObjectId - @@index([userId], map: "userId_1") + // Relations + user user @relation(fields: [userId], references: [id]) } model sessions { diff --git a/api/src/exam-environment/routes/exam-environment.ts b/api/src/exam-environment/routes/exam-environment.ts index aa6832b48ab..734924a2799 100644 --- a/api/src/exam-environment/routes/exam-environment.ts +++ b/api/src/exam-environment/routes/exam-environment.ts @@ -107,7 +107,7 @@ async function tokenVerifyHandler( const examEnvironmentAuthorizationToken = payload.examEnvironmentAuthorizationToken; - const token = await this.prisma.examEnvironmentAuthorizationToken.findFirst({ + const token = await this.prisma.examEnvironmentAuthorizationToken.findUnique({ where: { id: examEnvironmentAuthorizationToken } diff --git a/api/src/routes/protected/user.test.ts b/api/src/routes/protected/user.test.ts index 546d936de58..a8014cfbd64 100644 --- a/api/src/routes/protected/user.test.ts +++ b/api/src/routes/protected/user.test.ts @@ -16,6 +16,7 @@ import { createSuperRequest } from '../../../jest.utils'; import { JWT_SECRET } from '../../utils/env'; +import { customNanoid } from '../../utils/ids'; import { getMsTranscriptApiUrl } from './user'; const mockedFetch = jest.fn(); @@ -564,6 +565,11 @@ describe('userRoutes', () => { await fastifyTestInstance.prisma.userToken.deleteMany({ where: { id: userTokenId } }); + await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany( + { + where: { userId: defaultUserId } + } + ); }); test('GET rejects with 500 status code if the username is missing', async () => { @@ -1130,6 +1136,72 @@ Thanks and regards, }); }); }); + + describe('/user/exam-environment/token', () => { + afterEach(async () => { + await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany( + { + where: { userId: defaultUserId } + } + ); + }); + + test('POST generates a new token if one does not exist', async () => { + const response = await superPost('/user/exam-environment/token'); + const { examEnvironmentAuthorizationToken } = response.body.data; + + const decodedToken = jwt.decode(examEnvironmentAuthorizationToken); + + expect(decodedToken).toStrictEqual({ + examEnvironmentAuthorizationToken: + expect.stringMatching(/^[a-zA-Z0-9]{64}$/), + iat: expect.any(Number) + }); + + expect(() => + jwt.verify(examEnvironmentAuthorizationToken, 'wrong-secret') + ).toThrow(); + expect(() => + jwt.verify(examEnvironmentAuthorizationToken, JWT_SECRET) + ).not.toThrow(); + + expect(response.status).toBe(200); + }); + + test('POST only allows for one token per user id', async () => { + const id = customNanoid(); + await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.create( + { + data: { + userId: defaultUserId, + id, + createdDate: new Date() + } + } + ); + + const response = await superPost('/user/exam-environment/token'); + + const { examEnvironmentAuthorizationToken } = response.body.data; + + const decodedToken = jwt.decode(examEnvironmentAuthorizationToken); + + expect(decodedToken).not.toHaveProperty( + 'examEnvironmentAuthorizationToken', + id + ); + + expect(response.status).toBe(200); + + const tokens = + await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.findMany( + { + where: { userId: defaultUserId } + } + ); + expect(tokens).toHaveLength(1); + }); + }); }); describe('Unauthenticated user', () => {