Skip to content

Commit 9f675b0

Browse files
Merge pull request #734 from freeCodeCamp/main
Create a new pull request by comparing changes across two branches. If you
2 parents 58db4b7 + fc361c3 commit 9f675b0

File tree

1,129 files changed

+47907
-5760
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,129 files changed

+47907
-5760
lines changed

.github/CODEOWNERS

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ pnpm-lock.yaml
3434
# Files that need attention from Curriculum team
3535
# -------------------------------------------------
3636

37-
/curriculum/challenges/_meta/* @freecodecamp/curriculum
3837
/curriculum/challenges/english/* @freecodecamp/curriculum
39-
/client/i18n/locales/english/* @freecodecamp/curriculum
38+
/curriculum/challenges/structure/* @freecodecamp/dev-team @freecodecamp/curriculum
39+
/client/i18n/locales/english/* @freecodecamp/dev-team @freecodecamp/curriculum
40+
/client/src/pages/learn/* @freecodecamp/dev-team @freecodecamp/curriculum
4041

4142
# -------------------------------------------------
4243
# Files that need attention from i18n & dev team
@@ -52,3 +53,4 @@ pnpm-lock.yaml
5253

5354
/curriculum/schema/challenge-schema.js @freeCodeCamp/dev-team @freeCodeCamp/mobile
5455
/client/src/redux/prop-types.ts @freeCodeCamp/dev-team @freeCodeCamp/mobile
56+
/client/tools/external-curriculum/* @freeCodeCamp/dev-team @freeCodeCamp/mobile

api/prisma/schema.prisma

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ model user {
134134
is2018DataVisCert Boolean? // Undefined
135135
is2018FullStackCert Boolean? // Undefined
136136
isCollegeAlgebraPyCertV8 Boolean? // Undefined
137+
isFrontEndLibsCertV9 Boolean? // Undefined
138+
isBackEndDevApisCertV9 Boolean? // Undefined
139+
isFullStackDeveloperCertV9 Boolean? // Undefined
140+
isB1EnglishCert Boolean? // Undefined
141+
isA2SpanishCert Boolean? // Undefined
142+
isA2ChineseCert Boolean? // Undefined
143+
isA1ChineseCert Boolean? // Undefined
137144
// isUpcomingPythonCertV8 Boolean? // Undefined. It is in the db but has never been used.
138145
keyboardShortcuts Boolean? // Undefined
139146
linkedin String? // Null | Undefined

api/src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export const build = async (
224224

225225
void fastify.register(function (fastify, _opts, done) {
226226
fastify.addHook('onRequest', fastify.authorizeExamEnvironmentToken);
227+
fastify.addHook('onRequest', fastify.send401IfNoUser);
227228

228229
void fastify.register(examEnvironmentValidatedTokenRoutes);
229230
done();

api/src/exam-environment/routes/exam-environment.ts

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ import { isObjectID } from '../../utils/validation.js';
2424
*/
2525
export const examEnvironmentValidatedTokenRoutes: FastifyPluginCallbackTypebox =
2626
(fastify, _options, done) => {
27+
fastify.setErrorHandler((error, req, res) => {
28+
// If the error does not match the format {code: string; message: string}, coerce into:
29+
if (
30+
!Object.hasOwnProperty.call(error, 'code') ||
31+
!Object.hasOwnProperty.call(error, 'message')
32+
) {
33+
const logger = fastify.log.child({ req, res });
34+
logger.error(error, 'Unhandled error in exam environment routes.');
35+
const str = JSON.stringify(error);
36+
res.code(500);
37+
res.send(ERRORS.FCC_ERR_UNKNOWN_STATE(str));
38+
}
39+
});
40+
2741
fastify.get(
2842
'/exam-environment/exams',
2943
{
@@ -182,7 +196,16 @@ async function postExamGeneratedExamHandler(
182196
reply: FastifyReply
183197
) {
184198
const logger = this.log.child({ req });
185-
logger.info({ userId: req.user?.id });
199+
const user = req.user;
200+
201+
if (!user) {
202+
logger.error('No user found in request.');
203+
this.Sentry.captureException('No user found in request.');
204+
void reply.code(500);
205+
return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.'));
206+
}
207+
208+
logger.info({ userId: user.id });
186209
// Get exam from DB
187210
const examId = req.body.examId;
188211
const maybeExam = await mapErr(
@@ -218,7 +241,6 @@ async function postExamGeneratedExamHandler(
218241
}
219242

220243
// Check user has completed prerequisites
221-
const user = req.user!;
222244
const isExamPrerequisitesMet = checkPrerequisites(user, exam.prerequisites);
223245

224246
if (!isExamPrerequisitesMet) {
@@ -521,10 +543,18 @@ async function postExamAttemptHandler(
521543
reply: FastifyReply
522544
) {
523545
const logger = this.log.child({ req });
524-
logger.info({ userId: req.user?.id });
525-
const { attempt } = req.body;
546+
const user = req.user;
526547

527-
const user = req.user!;
548+
if (!user) {
549+
logger.error('No user found in request.');
550+
this.Sentry.captureException('No user found in request.');
551+
void reply.code(500);
552+
return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.'));
553+
}
554+
555+
logger.info({ userId: user.id });
556+
557+
const { attempt } = req.body;
528558

529559
const maybeAttempts = await mapErr(
530560
this.prisma.examEnvironmentExamAttempt.findMany({
@@ -651,23 +681,25 @@ async function postExamAttemptHandler(
651681
);
652682

653683
if (maybeValidExamAttempt.hasError) {
654-
logger.warn(
655-
{ validExamAttemptError: maybeValidExamAttempt.error },
656-
'Invalid exam attempt.'
657-
);
658-
// As attempt is invalid, create moderation record to investigate
659-
await this.prisma.examEnvironmentExamModeration.create({
660-
data: {
684+
const message =
685+
maybeValidExamAttempt.error instanceof Error
686+
? maybeValidExamAttempt.error.message
687+
: 'Unknown attempt validation error';
688+
logger.warn({ validExamAttemptError: message }, 'Invalid exam attempt.');
689+
// As attempt is invalid, create moderation record to investigate or update existing record
690+
await this.prisma.examEnvironmentExamModeration.upsert({
691+
where: { examAttemptId: latestAttempt.id },
692+
create: {
661693
examAttemptId: latestAttempt.id,
662-
status: ExamEnvironmentExamModerationStatus.Pending
694+
status: ExamEnvironmentExamModerationStatus.Pending,
695+
feedback: message
696+
},
697+
update: {
698+
feedback: message
663699
}
664700
});
665701

666702
void reply.code(400);
667-
const message =
668-
maybeValidExamAttempt.error instanceof Error
669-
? maybeValidExamAttempt.error.message
670-
: 'Unknown attempt validation error';
671703
return reply.send(ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_EXAM_ATTEMPT(message));
672704
}
673705

@@ -704,9 +736,17 @@ export async function getExams(
704736
reply: FastifyReply
705737
) {
706738
const logger = this.log.child({ req });
707-
logger.info({ userId: req.user?.id });
739+
const user = req.user;
740+
741+
if (!user) {
742+
logger.error('No user found in request.');
743+
this.Sentry.captureException('No user found in request.');
744+
void reply.code(500);
745+
return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.'));
746+
}
747+
748+
logger.info({ userId: user.id });
708749

709-
const user = req.user!;
710750
const maybeExams = await mapErr(
711751
this.prisma.examEnvironmentExam.findMany({
712752
where: {
@@ -869,9 +909,16 @@ export async function getExamAttemptsHandler(
869909
reply: FastifyReply
870910
) {
871911
const logger = this.log.child({ req });
872-
logger.info({ userId: req.user?.id });
912+
const user = req.user;
873913

874-
const user = req.user!;
914+
if (!user) {
915+
logger.error('No user found in request.');
916+
this.Sentry.captureException('No user found in request.');
917+
void reply.code(500);
918+
return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.'));
919+
}
920+
921+
logger.info({ userId: user.id });
875922

876923
// Send all relevant exam attempts
877924
const envExamAttempts = [];
@@ -929,9 +976,16 @@ export async function getExamAttemptHandler(
929976
reply: FastifyReply
930977
) {
931978
const logger = this.log.child({ req });
932-
logger.info({ userId: req.user?.id });
979+
const user = req.user;
980+
981+
if (!user) {
982+
logger.error('No user found in request.');
983+
this.Sentry.captureException('No user found in request.');
984+
void reply.code(500);
985+
return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.'));
986+
}
987+
logger.info({ userId: user.id });
933988

934-
const user = req.user!;
935989
const { attemptId } = req.params;
936990

937991
// If attempt id is given, only return that attempt
@@ -988,8 +1042,15 @@ export async function getExamAttemptsByExamIdHandler(
9881042
reply: FastifyReply
9891043
) {
9901044
const logger = this.log.child({ req });
1045+
const user = req.user;
1046+
1047+
if (!user) {
1048+
logger.error('No user found in request.');
1049+
this.Sentry.captureException('No user found in request.');
1050+
void reply.code(500);
1051+
return reply.send(ERRORS.FCC_ERR_UNKNOWN_STATE('No user found.'));
1052+
}
9911053

992-
const user = req.user!;
9931054
const { examId } = req.params;
9941055

9951056
logger.info({ examId, userId: user.id });

api/src/exam-environment/utils/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export const ERRORS = {
3939
'FCC_ENOENT_EXAM_ENVIRONMENT_GENERATED_EXAM',
4040
'%s'
4141
),
42-
FCC_EINVAL_EXAM_ID: createError('FCC_EINVAL_EXAM_ID', '%s')
42+
FCC_EINVAL_EXAM_ID: createError('FCC_EINVAL_EXAM_ID', '%s'),
43+
FCC_ERR_UNKNOWN_STATE: createError('FCC_ERR_UNKNOWN_STATE', '%s')
4344
};
4445

4546
/**

api/src/plugins/__fixtures__/user.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export const newUser = (email: string) => ({
5656
isRespWebDesignCert: false,
5757
isRespWebDesignCertV9: false,
5858
isSciCompPyCertV7: false,
59+
isFrontEndLibsCertV9: false,
60+
isBackEndDevApisCertV9: false,
61+
isFullStackDeveloperCertV9: false,
62+
isB1EnglishCert: false,
63+
isA2SpanishCert: false,
64+
isA2ChineseCert: false,
65+
isA1ChineseCert: false,
5966
keyboardShortcuts: false,
6067
linkedin: null,
6168
location: '',

api/src/plugins/auth.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,12 @@ const auth: FastifyPluginCallback = (fastify, _options, done) => {
156156
});
157157

158158
if (!token) {
159-
return {
160-
message: 'Token not found'
161-
};
159+
void reply.code(403);
160+
return reply.send(
161+
ERRORS.FCC_ENOENT_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN(
162+
'Provided token is revoked.'
163+
)
164+
);
162165
}
163166
// We're using token.userId since it's possible for the user record to be
164167
// malformed and for prisma to throw while trying to find the user.

api/src/routes/helpers/certificate-utils.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ const fullStackCertificateIds = [
2121
* @param certSlug - The certification slug to check.
2222
* @returns True if the certification slug is known, otherwise false.
2323
*/
24-
export function isKnownCertSlug(
25-
certSlug: string
26-
): certSlug is keyof typeof certSlugTypeMap {
24+
export function isKnownCertSlug(certSlug: string): certSlug is Certification {
2725
return certSlug in certSlugTypeMap;
2826
}
2927

api/src/routes/helpers/user-utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ const nullableFlags = [
3434
'isRespWebDesignCertV9',
3535
'isSciCompPyCertV7',
3636
'isDataAnalysisPyCertV7',
37+
'isFrontEndLibsCertV9',
38+
'isBackEndDevApisCertV9',
39+
'isFullStackDeveloperCertV9',
40+
'isB1EnglishCert',
41+
'isA2SpanishCert',
42+
'isA2ChineseCert',
43+
'isA1ChineseCert',
3744
// isUpcomingPythonCertV8 exists in the db, but is not returned by the api-server
3845
// TODO(Post-MVP): delete it from the db?
3946
'keyboardShortcuts'

api/src/routes/protected/certificate.test.ts

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
defaultUserEmail,
1414
defaultUserId,
1515
devLogin,
16+
resetDefaultUser,
1617
setupServer,
1718
superRequest
1819
} from '../../../vitest.utils.js';
@@ -35,30 +36,13 @@ describe('certificate routes', () => {
3536

3637
describe('PUT /certificate/verify', () => {
3738
beforeEach(async () => {
39+
await resetDefaultUser();
3840
await fastifyTestInstance.prisma.user.updateMany({
3941
where: { email: defaultUserEmail },
4042
data: {
41-
completedChallenges: [],
42-
is2018DataVisCert: false,
43-
isA2EnglishCert: false,
44-
isApisMicroservicesCert: false,
45-
isCollegeAlgebraPyCertV8: false,
46-
isDataAnalysisPyCertV7: false,
47-
isFoundationalCSharpCertV8: false,
48-
isFrontEndLibsCert: false,
49-
isInfosecCertV7: false,
50-
isJsAlgoDataStructCert: false,
51-
isJavascriptCertV9: false,
52-
isMachineLearningPyCertV7: false,
53-
isPythonCertV9: false,
54-
isQaCertV7: false,
55-
isRelationalDatabaseCertV8: false,
56-
isRelationalDatabaseCertV9: false,
57-
isRespWebDesignCert: false,
58-
isRespWebDesignCertV9: false,
59-
isSciCompPyCertV7: false,
6043
name: 'fcc',
61-
username: 'fcc'
44+
username: 'fcc',
45+
completedChallenges: []
6246
}
6347
});
6448
});
@@ -174,7 +158,6 @@ describe('certificate routes', () => {
174158
await fastifyTestInstance.prisma.user.updateMany({
175159
where: { email: defaultUserEmail },
176160
data: {
177-
completedChallenges: [],
178161
isRespWebDesignCert: true
179162
}
180163
});
@@ -309,29 +292,37 @@ describe('certificate routes', () => {
309292
}
310293
},
311294
isCertMap: {
312-
isA2EnglishCert: false,
313-
isRespWebDesignCert: true,
314-
isRespWebDesignCertV9: false,
315-
isJavascriptCertV9: false,
316-
isJsAlgoDataStructCert: false,
317-
isFrontEndLibsCert: false,
318295
is2018DataVisCert: false,
296+
isA1ChineseCert: false,
297+
isA2ChineseCert: false,
298+
isA2EnglishCert: false,
299+
isA2SpanishCert: false,
319300
isApisMicroservicesCert: false,
320-
isInfosecQaCert: false,
321-
isQaCertV7: false,
322-
isInfosecCertV7: false,
323-
isFrontEndCert: false,
301+
isB1EnglishCert: false,
324302
isBackEndCert: false,
303+
isBackEndDevApisCertV9: false,
304+
isCollegeAlgebraPyCertV8: false,
305+
isDataAnalysisPyCertV7: false,
325306
isDataVisCert: false,
307+
isFoundationalCSharpCertV8: false,
308+
isFrontEndCert: false,
309+
isFrontEndLibsCert: false,
310+
isFrontEndLibsCertV9: false,
326311
isFullStackCert: false,
327-
isSciCompPyCertV7: false,
328-
isDataAnalysisPyCertV7: false,
312+
isFullStackDeveloperCertV9: false,
313+
isInfosecCertV7: false,
314+
isInfosecQaCert: false,
315+
isJavascriptCertV9: false,
316+
isJsAlgoDataStructCert: false,
317+
isJsAlgoDataStructCertV8: false,
329318
isMachineLearningPyCertV7: false,
330-
isRelationalDatabaseCertV8: false,
331-
isCollegeAlgebraPyCertV8: false,
332-
isFoundationalCSharpCertV8: false,
333319
isPythonCertV9: false,
334-
isRelationalDatabaseCertV9: false
320+
isQaCertV7: false,
321+
isRelationalDatabaseCertV8: false,
322+
isRelationalDatabaseCertV9: false,
323+
isRespWebDesignCert: true,
324+
isRespWebDesignCertV9: false,
325+
isSciCompPyCertV7: false
335326
},
336327
completedChallenges: [
337328
{

0 commit comments

Comments
 (0)