diff --git a/api/src/app.ts b/api/src/app.ts index 21f7c04790a..fa2dbf68154 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -105,7 +105,10 @@ export const build = async ( // test environments) skipInit: !SENTRY_DSN, errorResponse: (error, _request, reply) => { - if (reply.statusCode === 500) { + const isCSRFError = + error.code === 'FST_CSRF_INVALID_TOKEN' || + error.code === 'FST_CSRF_MISSING_SECRET'; + if (reply.statusCode === 500 || isCSRFError) { void reply.send({ message: 'flash.generic-error', type: 'danger' diff --git a/api/src/routes/challenge.ts b/api/src/routes/challenge.ts index 096450d6c43..b29fcfc7b2a 100644 --- a/api/src/routes/challenge.ts +++ b/api/src/routes/challenge.ts @@ -396,8 +396,7 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = ( !multifileCertProjectIds.includes(challengeId) && !multifilePythonCertProjectIds.includes(challengeId) ) { - void reply.code(403); - return 'That challenge type is not saveable.'; + void reply.code(403).send('That challenge type is not saveable.'); } const userSavedChallenges = saveUserChallengeData( @@ -413,7 +412,7 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = ( } }); - return { savedChallenges: userSavedChallenges }; + void reply.send({ savedChallenges: userSavedChallenges }); } ); diff --git a/api/src/routes/settings.test.ts b/api/src/routes/settings.test.ts index a82a3c4847e..23aa8abc70c 100644 --- a/api/src/routes/settings.test.ts +++ b/api/src/routes/settings.test.ts @@ -62,10 +62,8 @@ describe('settingRoutes', () => { }); expect(response.body).toEqual({ - code: 'FST_CSRF_MISSING_SECRET', - error: 'Forbidden', - message: 'Missing csrf secret', - statusCode: 403 + message: 'flash.generic-error', + type: 'danger' }); expect(response.statusCode).toEqual(403); }); @@ -76,10 +74,8 @@ describe('settingRoutes', () => { }).set('Cookie', ['_csrf=foo', 'csrf-token=bar']); expect(response.body).toEqual({ - code: 'FST_CSRF_INVALID_TOKEN', - error: 'Forbidden', - message: 'Invalid csrf token', - statusCode: 403 + message: 'flash.generic-error', + type: 'danger' }); expect(response.statusCode).toEqual(403); }); diff --git a/api/src/schemas/certificate/cert-slug.ts b/api/src/schemas/certificate/cert-slug.ts index 425834994ac..402fb2fac40 100644 --- a/api/src/schemas/certificate/cert-slug.ts +++ b/api/src/schemas/certificate/cert-slug.ts @@ -1,6 +1,6 @@ import { Type } from '@fastify/type-provider-typebox'; import { Certification } from '../../../../shared/config/certification-settings'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const certSlug = { params: Type.Object({ @@ -120,6 +120,6 @@ export const certSlug = { }) ) }), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/certificate/certificate-verify.ts b/api/src/schemas/certificate/certificate-verify.ts index a790154ea6d..9825544a89b 100644 --- a/api/src/schemas/certificate/certificate-verify.ts +++ b/api/src/schemas/certificate/certificate-verify.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500, isCertMap } from '../types'; +import { genericError, isCertMap } from '../types'; export const certificateVerify = { // TODO(POST_MVP): Remove partial validation from route for schema validation @@ -126,7 +126,8 @@ export const certificateVerify = { type: Type.Literal('danger'), message: Type.Literal('flash.went-wrong') }), - generic500 - ]) + genericError + ]), + default: genericError } }; diff --git a/api/src/schemas/challenge/backend-challenge-completed.ts b/api/src/schemas/challenge/backend-challenge-completed.ts index e42149be78b..65a2a2617db 100644 --- a/api/src/schemas/challenge/backend-challenge-completed.ts +++ b/api/src/schemas/challenge/backend-challenge-completed.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const backendChallengeCompleted = { body: Type.Object({ @@ -17,6 +17,6 @@ export const backendChallengeCompleted = { 'That does not appear to be a valid challenge submission.' ) }), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/challenge/coderoad-challenge-completed.ts b/api/src/schemas/challenge/coderoad-challenge-completed.ts index 8bdf5ee0c91..f8ba0e2ed1e 100644 --- a/api/src/schemas/challenge/coderoad-challenge-completed.ts +++ b/api/src/schemas/challenge/coderoad-challenge-completed.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const coderoadChallengeCompleted = { body: Type.Object({ @@ -15,6 +15,6 @@ export const coderoadChallengeCompleted = { type: Type.Literal('error'), msg: Type.String() }), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/challenge/exam-challenge-completed.ts b/api/src/schemas/challenge/exam-challenge-completed.ts index 5fa51db497f..af161ee8447 100644 --- a/api/src/schemas/challenge/exam-challenge-completed.ts +++ b/api/src/schemas/challenge/exam-challenge-completed.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { examResults } from '../types'; +import { examResults, genericError } from '../types'; export const examChallengeCompleted = { body: Type.Object({ @@ -30,9 +30,12 @@ export const examChallengeCompleted = { 400: Type.Object({ error: Type.String() }), - 403: Type.Object({ - error: Type.String() - }), + 403: Type.Union([ + Type.Object({ + error: Type.String() + }), + genericError + ]), 500: Type.Object({ error: Type.String() }) diff --git a/api/src/schemas/challenge/exam.ts b/api/src/schemas/challenge/exam.ts index dec30384e03..ca48befe526 100644 --- a/api/src/schemas/challenge/exam.ts +++ b/api/src/schemas/challenge/exam.ts @@ -1,4 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; +import { genericError } from '../types'; export const exam = { params: Type.Object({ @@ -27,9 +28,12 @@ export const exam = { 400: Type.Object({ error: Type.String() }), - 403: Type.Object({ - error: Type.String() - }), + 403: Type.Union([ + Type.Object({ + error: Type.String() + }), + genericError + ]), 500: Type.Object({ error: Type.String() }) diff --git a/api/src/schemas/challenge/modern-challenge-completed.ts b/api/src/schemas/challenge/modern-challenge-completed.ts index 5e71d9575dc..da6c5d8fddb 100644 --- a/api/src/schemas/challenge/modern-challenge-completed.ts +++ b/api/src/schemas/challenge/modern-challenge-completed.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500, savedChallenge } from '../types'; +import { genericError, savedChallenge } from '../types'; export const modernChallengeCompleted = { body: Type.Object({ @@ -30,6 +30,6 @@ export const modernChallengeCompleted = { 'That does not appear to be a valid challenge submission.' ) }), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/challenge/project-completed.ts b/api/src/schemas/challenge/project-completed.ts index 60969db825d..d7230c2baad 100644 --- a/api/src/schemas/challenge/project-completed.ts +++ b/api/src/schemas/challenge/project-completed.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const projectCompleted = { body: Type.Object({ @@ -28,15 +28,20 @@ export const projectCompleted = { ) ]) }), - 403: Type.Object({ - type: Type.Literal('error'), - message: Type.Union([ - Type.Literal( - 'You have to complete the project before you can submit a URL.' - ), - Type.Literal('That does not appear to be a valid challenge submission.') - ]) - }), - 500: generic500 + 403: Type.Union([ + Type.Object({ + type: Type.Literal('error'), + message: Type.Union([ + Type.Literal( + 'You have to complete the project before you can submit a URL.' + ), + Type.Literal( + 'That does not appear to be a valid challenge submission.' + ) + ]) + }), + genericError + ]), + default: genericError } }; diff --git a/api/src/schemas/challenge/save-challenge.ts b/api/src/schemas/challenge/save-challenge.ts index 24b987a398e..93b27a8aad8 100644 --- a/api/src/schemas/challenge/save-challenge.ts +++ b/api/src/schemas/challenge/save-challenge.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { file, generic500, savedChallenge } from '../types'; +import { file, genericError, savedChallenge } from '../types'; export const saveChallenge = { body: Type.Object({ @@ -14,7 +14,16 @@ export const saveChallenge = { 200: Type.Object({ savedChallenges: Type.Array(savedChallenge) }), - 403: Type.Literal('That challenge type is not saveable.'), - 500: generic500 + 400: Type.Object({ + message: Type.Literal( + 'That does not appear to be a valid challenge submission.' + ), + type: Type.Literal('error') + }), + 403: Type.Union([ + Type.Literal('That challenge type is not saveable.'), + genericError + ]), + default: genericError } }; diff --git a/api/src/schemas/settings/update-my-classroom-mode.ts b/api/src/schemas/settings/update-my-classroom-mode.ts index 1427e8eb0c2..76d1db7520f 100644 --- a/api/src/schemas/settings/update-my-classroom-mode.ts +++ b/api/src/schemas/settings/update-my-classroom-mode.ts @@ -1,4 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; +import { genericError } from '../types'; export const updateMyClassroomMode = { body: Type.Object({ @@ -9,9 +10,12 @@ export const updateMyClassroomMode = { message: Type.Literal('flash.classroom-mode-updated'), type: Type.Literal('success') }), - 403: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) + 403: Type.Union([ + Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }), + genericError + ]) } }; diff --git a/api/src/schemas/types.ts b/api/src/schemas/types.ts index 5dc45a7048c..94e90db0ff7 100644 --- a/api/src/schemas/types.ts +++ b/api/src/schemas/types.ts @@ -1,6 +1,6 @@ import { Type } from '@fastify/type-provider-typebox'; -export const generic500 = Type.Object({ +export const genericError = Type.Object({ message: Type.Literal('flash.generic-error'), type: Type.Literal('danger') }); diff --git a/api/src/schemas/user/delete-my-account.ts b/api/src/schemas/user/delete-my-account.ts index c83ed5e2471..8004a7c6aaa 100644 --- a/api/src/schemas/user/delete-my-account.ts +++ b/api/src/schemas/user/delete-my-account.ts @@ -1,9 +1,9 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const deleteMyAccount = { response: { 200: Type.Object({}), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/user/delete-user-token.ts b/api/src/schemas/user/delete-user-token.ts index 3012ed56c00..5ddfa8c2248 100644 --- a/api/src/schemas/user/delete-user-token.ts +++ b/api/src/schemas/user/delete-user-token.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const deleteUserToken = { response: { @@ -10,6 +10,6 @@ export const deleteUserToken = { message: Type.Literal('userToken not found'), type: Type.Literal('info') }), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/user/post-ms-username.ts b/api/src/schemas/user/post-ms-username.ts index d8bf2045810..5791871f5af 100644 --- a/api/src/schemas/user/post-ms-username.ts +++ b/api/src/schemas/user/post-ms-username.ts @@ -1,4 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; +import { genericError } from '../types'; export const postMsUsername = { body: Type.Object({ @@ -16,10 +17,13 @@ export const postMsUsername = { type: Type.Literal('error'), message: Type.Literal('flash.ms.transcript.link-err-2') }), - 403: Type.Object({ - type: Type.Literal('error'), - message: Type.Literal('flash.ms.transcript.link-err-4') - }), + 403: Type.Union([ + Type.Object({ + type: Type.Literal('error'), + message: Type.Literal('flash.ms.transcript.link-err-4') + }), + genericError + ]), 500: Type.Union([ Type.Object({ type: Type.Literal('error'), diff --git a/api/src/schemas/user/report-user.ts b/api/src/schemas/user/report-user.ts index 4d19114fa44..f2523b264cf 100644 --- a/api/src/schemas/user/report-user.ts +++ b/api/src/schemas/user/report-user.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const reportUser = { body: Type.Object({ @@ -18,6 +18,6 @@ export const reportUser = { type: Type.Literal('danger'), message: Type.Literal('flash.provide-username') }), - 500: generic500 + default: genericError } }; diff --git a/api/src/schemas/user/reset-my-progress.ts b/api/src/schemas/user/reset-my-progress.ts index f174c005e77..0dcbb5ca253 100644 --- a/api/src/schemas/user/reset-my-progress.ts +++ b/api/src/schemas/user/reset-my-progress.ts @@ -1,9 +1,9 @@ import { Type } from '@fastify/type-provider-typebox'; -import { generic500 } from '../types'; +import { genericError } from '../types'; export const resetMyProgress = { response: { 200: Type.Object({}), - 500: generic500 + default: genericError } }; diff --git a/api/src/utils/error-formatting.ts b/api/src/utils/error-formatting.ts index 43a05b3e5f2..3c671d0f926 100644 --- a/api/src/utils/error-formatting.ts +++ b/api/src/utils/error-formatting.ts @@ -34,6 +34,8 @@ export const formatProjectCompletedValidation = ( ): FormattedError => { const error = getError(errors); + // TODO: split this into two functions. There's no need for it to handle both + // /project-completed and /save-challenge return error.instancePath === '' && error.params.missingProperty === 'solution' ? {