diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index cec7cf4c82c..188a3ea11ff 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -379,9 +379,11 @@ model UserToken { } model ExamEnvironmentAuthorizationToken { - id String @id @map("_id") - createdDate DateTime @db.Date - userId String @unique @db.ObjectId + /// An ObjectId is used to provide access to the created timestamp + id String @id @default(auto()) @map("_id") @db.ObjectId + /// Used to set an `expireAt` index to delete documents + expireAt DateTime @db.Date + userId String @unique @db.ObjectId // Relations user user @relation(fields: [userId], references: [id]) diff --git a/api/src/exam-environment/routes/exam-environment.test.ts b/api/src/exam-environment/routes/exam-environment.test.ts index d235046baf7..cca82576ef2 100644 --- a/api/src/exam-environment/routes/exam-environment.test.ts +++ b/api/src/exam-environment/routes/exam-environment.test.ts @@ -35,11 +35,11 @@ describe('/exam-environment/', () => { await mock.seedEnvExam(); // Add exam environment authorization token const res = await superPost('/user/exam-environment/token'); - expect(res.status).toBe(200); + expect(res.status).toBe(201); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment examEnvironmentAuthorizationToken = // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - res.body.data.examEnvironmentAuthorizationToken; + res.body.examEnvironmentAuthorizationToken; }); describe('POST /exam-environment/exam/attempt', () => { @@ -389,11 +389,9 @@ describe('/exam-environment/', () => { expect(res).toMatchObject({ status: 200, body: { - data: { - examAttempt: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - id: expect.not.stringMatching(mock.examAttempt.id) - } + examAttempt: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + id: expect.not.stringMatching(mock.examAttempt.id) } } }); @@ -419,9 +417,7 @@ describe('/exam-environment/', () => { expect(res).toMatchObject({ status: 200, body: { - data: { - examAttempt: latestAttempt - } + examAttempt: latestAttempt } }); }); @@ -555,10 +551,8 @@ describe('/exam-environment/', () => { expect(res).toMatchObject({ status: 200, body: { - data: { - examAttempt, - exam: userExam - } + examAttempt, + exam: userExam } }); }); @@ -644,15 +638,15 @@ describe('/exam-environment/', () => { }); }); - describe('POST /exam-environment/token/verify', () => { + describe('GET /exam-environment/token-meta', () => { it('should allow a valid request', async () => { - const res = await superPost('/exam-environment/token/verify').set( + const res = await superGet('/exam-environment/token-meta').set( 'exam-environment-authorization-token', 'invalid-token' ); expect(res).toMatchObject({ - status: 200, + status: 418, body: { code: 'FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN' } diff --git a/api/src/exam-environment/routes/exam-environment.ts b/api/src/exam-environment/routes/exam-environment.ts index 734924a2799..f8fdcce6726 100644 --- a/api/src/exam-environment/routes/exam-environment.ts +++ b/api/src/exam-environment/routes/exam-environment.ts @@ -64,12 +64,12 @@ export const examEnvironmentOpenRoutes: FastifyPluginCallbackTypebox = ( _options, done ) => { - fastify.post( - '/exam-environment/token/verify', + fastify.get( + '/exam-environment/token-meta', { - schema: schemas.examEnvironmentTokenVerify + schema: schemas.examEnvironmentTokenMeta }, - tokenVerifyHandler + tokenMetaHandler ); done(); }; @@ -85,9 +85,9 @@ interface JwtPayload { * * **Note**: This has no guarantees of which user the token is for. Just that one exists in the database. */ -async function tokenVerifyHandler( +async function tokenMetaHandler( this: FastifyInstance, - req: UpdateReqType, + req: UpdateReqType, reply: FastifyReply ) { const { 'exam-environment-authorization-token': encodedToken } = req.headers; @@ -95,8 +95,8 @@ async function tokenVerifyHandler( try { jwt.verify(encodedToken, JWT_SECRET); } catch (e) { - // TODO: What to send back here? Request is valid, but token is not? - void reply.code(200); + // Server refuses to brew (verify) coffee (jwts) with a teapot (random strings) + void reply.code(418); return reply.send( ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN(JSON.stringify(e)) ); @@ -114,16 +114,17 @@ async function tokenVerifyHandler( }); if (!token) { - void reply.code(200); - return reply.send({ - data: 'Token does not appear to have been created.' - }); + // Endpoint is valid, but resource does not exists + void reply.code(404); + return reply.send( + ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN( + 'Token does not appear to exist' + ) + ); } else { void reply.code(200); return reply.send({ - data: { - createdDate: token.createdDate - } + expireAt: token.expireAt }); } } @@ -257,10 +258,8 @@ async function postExamGeneratedExamHandler( const userExam = constructUserExam(generated.data, exam); return reply.send({ - data: { - exam: userExam, - examAttempt: lastAttempt - } + exam: userExam, + examAttempt: lastAttempt }); } } @@ -375,10 +374,8 @@ async function postExamGeneratedExamHandler( void reply.code(200); return reply.send({ - data: { - exam: userExam, - examAttempt: attempt.data - } + exam: userExam, + examAttempt: attempt.data }); } diff --git a/api/src/exam-environment/schemas/exam-generated-exam.ts b/api/src/exam-environment/schemas/exam-generated-exam.ts index d0f45adb940..4e3ba3926d5 100644 --- a/api/src/exam-environment/schemas/exam-generated-exam.ts +++ b/api/src/exam-environment/schemas/exam-generated-exam.ts @@ -10,10 +10,8 @@ export const examEnvironmentPostExamGeneratedExam = { }), response: { 200: Type.Object({ - data: Type.Object({ - exam: Type.Record(Type.String(), Type.Unknown()), - examAttempt: Type.Record(Type.String(), Type.Unknown()) - }) + exam: Type.Record(Type.String(), Type.Unknown()), + examAttempt: Type.Record(Type.String(), Type.Unknown()) }), 403: STANDARD_ERROR, 404: STANDARD_ERROR, diff --git a/api/src/exam-environment/schemas/index.ts b/api/src/exam-environment/schemas/index.ts index 7cf2e1616c0..8f321bd9004 100644 --- a/api/src/exam-environment/schemas/index.ts +++ b/api/src/exam-environment/schemas/index.ts @@ -1,5 +1,5 @@ export { examEnvironmentPostExamAttempt } from './exam-attempt'; export { examEnvironmentPostExamGeneratedExam } from './exam-generated-exam'; export { examEnvironmentPostScreenshot } from './screenshot'; -export { examEnvironmentTokenVerify } from './token-verify'; +export { examEnvironmentTokenMeta } from './token-meta'; export { examEnvironmentExams } from './exams'; diff --git a/api/src/exam-environment/schemas/token-meta.ts b/api/src/exam-environment/schemas/token-meta.ts new file mode 100644 index 00000000000..d98095045a8 --- /dev/null +++ b/api/src/exam-environment/schemas/token-meta.ts @@ -0,0 +1,15 @@ +import { Type } from '@fastify/type-provider-typebox'; +import { STANDARD_ERROR } from '../utils/errors'; + +export const examEnvironmentTokenMeta = { + headers: Type.Object({ + 'exam-environment-authorization-token': Type.String() + }), + response: { + 200: Type.Object({ + expireAt: Type.String({ format: 'date-time' }) + }), + 404: STANDARD_ERROR, + 418: STANDARD_ERROR + } +}; diff --git a/api/src/exam-environment/schemas/token-verify.ts b/api/src/exam-environment/schemas/token-verify.ts deleted file mode 100644 index d8042da65f1..00000000000 --- a/api/src/exam-environment/schemas/token-verify.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Type } from '@fastify/type-provider-typebox'; -import { STANDARD_ERROR } from '../utils/errors'; - -export const examEnvironmentTokenVerify = { - headers: Type.Object({ - 'exam-environment-authorization-token': Type.String() - }), - response: { - 200: Type.Union([ - Type.Object({ - data: Type.Union([ - Type.String(), - Type.Object({ - createdDate: Type.String({ format: 'date-time' }) - }) - ]) - }), - STANDARD_ERROR - ]) - } -}; diff --git a/api/src/routes/protected/user.test.ts b/api/src/routes/protected/user.test.ts index a8014cfbd64..65f288dad0f 100644 --- a/api/src/routes/protected/user.test.ts +++ b/api/src/routes/protected/user.test.ts @@ -16,7 +16,6 @@ import { createSuperRequest } from '../../../jest.utils'; import { JWT_SECRET } from '../../utils/env'; -import { customNanoid } from '../../utils/ids'; import { getMsTranscriptApiUrl } from './user'; const mockedFetch = jest.fn(); @@ -1148,13 +1147,13 @@ Thanks and regards, 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 { examEnvironmentAuthorizationToken } = response.body; const decodedToken = jwt.decode(examEnvironmentAuthorizationToken); expect(decodedToken).toStrictEqual({ examEnvironmentAuthorizationToken: - expect.stringMatching(/^[a-zA-Z0-9]{64}$/), + expect.stringMatching(/^[a-z0-9]{24}$/), iat: expect.any(Number) }); @@ -1165,33 +1164,32 @@ Thanks and regards, jwt.verify(examEnvironmentAuthorizationToken, JWT_SECRET) ).not.toThrow(); - expect(response.status).toBe(200); + expect(response.status).toBe(201); }); 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 token = + await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.create( + { + data: { + userId: defaultUserId, + expireAt: new Date() + } } - } - ); + ); const response = await superPost('/user/exam-environment/token'); - const { examEnvironmentAuthorizationToken } = response.body.data; + const { examEnvironmentAuthorizationToken } = response.body; const decodedToken = jwt.decode(examEnvironmentAuthorizationToken); expect(decodedToken).not.toHaveProperty( 'examEnvironmentAuthorizationToken', - id + token.id ); - expect(response.status).toBe(200); + expect(response.status).toBe(201); const tokens = await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.findMany( diff --git a/api/src/routes/protected/user.ts b/api/src/routes/protected/user.ts index 07c33e93063..a54de1a25e7 100644 --- a/api/src/routes/protected/user.ts +++ b/api/src/routes/protected/user.ts @@ -397,10 +397,11 @@ async function examEnvironmentTokenHandler( } }); + const ONE_YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000; + const token = await this.prisma.examEnvironmentAuthorizationToken.create({ data: { - createdDate: new Date(), - id: customNanoid(), + expireAt: new Date(Date.now() + ONE_YEAR_IN_MS), userId } }); @@ -410,10 +411,9 @@ async function examEnvironmentTokenHandler( JWT_SECRET ); + void reply.code(201); void reply.send({ - data: { - examEnvironmentAuthorizationToken - } + examEnvironmentAuthorizationToken }); } diff --git a/api/src/schemas/user/exam-environment-token.ts b/api/src/schemas/user/exam-environment-token.ts index c324802e861..2135c3086ef 100644 --- a/api/src/schemas/user/exam-environment-token.ts +++ b/api/src/schemas/user/exam-environment-token.ts @@ -3,9 +3,7 @@ import { Type } from '@fastify/type-provider-typebox'; export const userExamEnvironmentToken = { response: { 200: Type.Object({ - data: Type.Object({ - examEnvironmentAuthorizationToken: Type.String() - }) + examEnvironmentAuthorizationToken: Type.String() }) } };