From c8395eb1db5f5a6c0331998f37c815e611d9eabd Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Wed, 10 Dec 2025 15:00:18 +0200 Subject: [PATCH] fix(api): add 401 to non user for exam routes (#64396) --- api/src/app.ts | 1 + .../routes/exam-environment.ts | 83 ++++++++++++++++--- api/src/exam-environment/utils/errors.ts | 3 +- api/src/plugins/auth.ts | 9 +- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/api/src/app.ts b/api/src/app.ts index 423b3639951..90247d0ff3c 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -224,6 +224,7 @@ export const build = async ( void fastify.register(function (fastify, _opts, done) { fastify.addHook('onRequest', fastify.authorizeExamEnvironmentToken); + fastify.addHook('onRequest', fastify.send401IfNoUser); void fastify.register(examEnvironmentValidatedTokenRoutes); done(); diff --git a/api/src/exam-environment/routes/exam-environment.ts b/api/src/exam-environment/routes/exam-environment.ts index 323098eb4b5..498643c1d7e 100644 --- a/api/src/exam-environment/routes/exam-environment.ts +++ b/api/src/exam-environment/routes/exam-environment.ts @@ -24,6 +24,20 @@ import { isObjectID } from '../../utils/validation.js'; */ export const examEnvironmentValidatedTokenRoutes: FastifyPluginCallbackTypebox = (fastify, _options, done) => { + fastify.setErrorHandler((error, req, res) => { + // If the error does not match the format {code: string; message: string}, coerce into: + if ( + !Object.hasOwnProperty.call(error, 'code') || + !Object.hasOwnProperty.call(error, 'message') + ) { + const logger = fastify.log.child({ req, res }); + logger.error(error, 'Unhandled error in exam environment routes.'); + const str = JSON.stringify(error); + res.code(500); + res.send(ERRORS.FCC_ERR_UNKNOWN_STATE(str)); + } + }); + fastify.get( '/exam-environment/exams', { @@ -182,7 +196,16 @@ async function postExamGeneratedExamHandler( reply: FastifyReply ) { const logger = this.log.child({ req }); - logger.info({ userId: req.user?.id }); + const user = req.user; + + if (!user) { + logger.error('No user found in request.'); + this.Sentry.captureException('No user found in request.'); + void reply.code(500); + return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.')); + } + + logger.info({ userId: user.id }); // Get exam from DB const examId = req.body.examId; const maybeExam = await mapErr( @@ -218,7 +241,6 @@ async function postExamGeneratedExamHandler( } // Check user has completed prerequisites - const user = req.user!; const isExamPrerequisitesMet = checkPrerequisites(user, exam.prerequisites); if (!isExamPrerequisitesMet) { @@ -521,10 +543,18 @@ async function postExamAttemptHandler( reply: FastifyReply ) { const logger = this.log.child({ req }); - logger.info({ userId: req.user?.id }); - const { attempt } = req.body; + const user = req.user; - const user = req.user!; + if (!user) { + logger.error('No user found in request.'); + this.Sentry.captureException('No user found in request.'); + void reply.code(500); + return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.')); + } + + logger.info({ userId: user.id }); + + const { attempt } = req.body; const maybeAttempts = await mapErr( this.prisma.examEnvironmentExamAttempt.findMany({ @@ -704,9 +734,17 @@ export async function getExams( reply: FastifyReply ) { const logger = this.log.child({ req }); - logger.info({ userId: req.user?.id }); + const user = req.user; + + if (!user) { + logger.error('No user found in request.'); + this.Sentry.captureException('No user found in request.'); + void reply.code(500); + return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.')); + } + + logger.info({ userId: user.id }); - const user = req.user!; const maybeExams = await mapErr( this.prisma.examEnvironmentExam.findMany({ where: { @@ -869,9 +907,16 @@ export async function getExamAttemptsHandler( reply: FastifyReply ) { const logger = this.log.child({ req }); - logger.info({ userId: req.user?.id }); + const user = req.user; - const user = req.user!; + if (!user) { + logger.error('No user found in request.'); + this.Sentry.captureException('No user found in request.'); + void reply.code(500); + return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.')); + } + + logger.info({ userId: user.id }); // Send all relevant exam attempts const envExamAttempts = []; @@ -929,9 +974,16 @@ export async function getExamAttemptHandler( reply: FastifyReply ) { const logger = this.log.child({ req }); - logger.info({ userId: req.user?.id }); + const user = req.user; + + if (!user) { + logger.error('No user found in request.'); + this.Sentry.captureException('No user found in request.'); + void reply.code(500); + return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.')); + } + logger.info({ userId: user.id }); - const user = req.user!; const { attemptId } = req.params; // If attempt id is given, only return that attempt @@ -988,8 +1040,15 @@ export async function getExamAttemptsByExamIdHandler( reply: FastifyReply ) { const logger = this.log.child({ req }); + const user = req.user; + + if (!user) { + logger.error('No user found in request.'); + this.Sentry.captureException('No user found in request.'); + void reply.code(500); + return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.')); + } - const user = req.user!; const { examId } = req.params; logger.info({ examId, userId: user.id }); diff --git a/api/src/exam-environment/utils/errors.ts b/api/src/exam-environment/utils/errors.ts index 2775d7b9bd9..7ca34379c5c 100644 --- a/api/src/exam-environment/utils/errors.ts +++ b/api/src/exam-environment/utils/errors.ts @@ -39,7 +39,8 @@ export const ERRORS = { 'FCC_ENOENT_EXAM_ENVIRONMENT_GENERATED_EXAM', '%s' ), - FCC_EINVAL_EXAM_ID: createError('FCC_EINVAL_EXAM_ID', '%s') + FCC_EINVAL_EXAM_ID: createError('FCC_EINVAL_EXAM_ID', '%s'), + FCC_ERR_UNKNOWN_STATE: createError('FCC_ERR_UNKNOWN_STATE', '%s') }; /** diff --git a/api/src/plugins/auth.ts b/api/src/plugins/auth.ts index d59baedc4d1..0acf105d2c1 100644 --- a/api/src/plugins/auth.ts +++ b/api/src/plugins/auth.ts @@ -156,9 +156,12 @@ const auth: FastifyPluginCallback = (fastify, _options, done) => { }); if (!token) { - return { - message: 'Token not found' - }; + void reply.code(403); + return reply.send( + ERRORS.FCC_ENOENT_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN( + 'Provided token is revoked.' + ) + ); } // We're using token.userId since it's possible for the user record to be // malformed and for prisma to throw while trying to find the user.