Skip to content

Commit 925b8dd

Browse files
Merge pull request #719 from freeCodeCamp/main
Create a new pull request by comparing changes across two branches
2 parents 6e8b178 + 4f2d879 commit 925b8dd

File tree

433 files changed

+52047
-926
lines changed

Some content is hidden

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

433 files changed

+52047
-926
lines changed

.gitpod.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ ports:
3030
tasks:
3131
- before: |
3232
echo '
33-
export COOKIE_DOMAIN=gitpod.io
33+
export COOKIE_DOMAIN=.gitpod.io
3434
export HOME_LOCATION=$(gp url 8000)
3535
export API_LOCATION=$(gp url 3000)
3636
export CHALLENGE_EDITOR_API_LOCATION=$(gp url 3200)
@@ -45,7 +45,7 @@ tasks:
4545
docker compose up -d
4646
4747
- name: server
48-
before: export COOKIE_DOMAIN=gitpod.io && export HOME_LOCATION=$(gp url 8000) && export API_LOCATION=$(gp url 3000)
48+
before: export COOKIE_DOMAIN=.gitpod.io && export HOME_LOCATION=$(gp url 8000) && export API_LOCATION=$(gp url 3000)
4949
# init is not executed for prebuilt workspaces and restarts,
5050
# so we should put all the heavy initialization here
5151
init: >

api-server/src/server/boot/randomAPIs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ module.exports = function (app) {
7878
if (!unsubscribeId) {
7979
req.flash(
8080
'info',
81-
'We we unable to process this request, please check and try again'
81+
'We were unable to process this request, please check and try again'
8282
);
8383
res.redirect(origin);
8484
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ describe('/exam-environment/', () => {
2424
setupServer();
2525
describe('Authenticated user with exam environment authorization token', () => {
2626
let superPost: ReturnType<typeof createSuperRequest>;
27+
let superGet: ReturnType<typeof createSuperRequest>;
2728
let examEnvironmentAuthorizationToken: string;
2829

2930
// Authenticate user
3031
beforeAll(async () => {
3132
const setCookies = await devLogin();
3233
superPost = createSuperRequest({ method: 'POST', setCookies });
34+
superGet = createSuperRequest({ method: 'GET', setCookies });
3335
await mock.seedEnvExam();
3436
// Add exam environment authorization token
3537
const res = await superPost('/user/exam-environment/token');
@@ -532,15 +534,43 @@ describe('/exam-environment/', () => {
532534
});
533535

534536
xdescribe('POST /exam-environment/screenshot', () => {});
537+
538+
describe('GET /exam-environment/exams', () => {
539+
it('should return 200', async () => {
540+
const res = await superGet('/exam-environment/exams').set(
541+
'exam-environment-authorization-token',
542+
examEnvironmentAuthorizationToken
543+
);
544+
expect(res.status).toBe(200);
545+
546+
expect(res.body).toStrictEqual({
547+
data: {
548+
exams: [
549+
{
550+
canTake: true,
551+
config: {
552+
name: mock.exam.config.name,
553+
note: mock.exam.config.note,
554+
totalTimeInMS: mock.exam.config.totalTimeInMS
555+
},
556+
id: mock.examId
557+
}
558+
]
559+
}
560+
});
561+
});
562+
});
535563
});
536564

537565
describe('Authenticated user without exam environment authorization token', () => {
538566
let superPost: ReturnType<typeof createSuperRequest>;
567+
let superGet: ReturnType<typeof createSuperRequest>;
539568

540569
// Authenticate user
541570
beforeAll(async () => {
542571
const setCookies = await devLogin();
543572
superPost = createSuperRequest({ method: 'POST', setCookies });
573+
superGet = createSuperRequest({ method: 'GET', setCookies });
544574
await mock.seedEnvExam();
545575
});
546576
describe('POST /exam-environment/exam/attempt', () => {
@@ -598,5 +628,16 @@ describe('/exam-environment/', () => {
598628
});
599629
});
600630
});
631+
632+
describe('GET /exam-environment/exams', () => {
633+
it('should return 403', async () => {
634+
const res = await superGet('/exam-environment/exams').set(
635+
'exam-environment-authorization-token',
636+
'invalid-token'
637+
);
638+
639+
expect(res.status).toBe(403);
640+
});
641+
});
601642
});
602643
});

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import { ERRORS } from '../utils/errors';
2323
*/
2424
export const examEnvironmentValidatedTokenRoutes: FastifyPluginCallbackTypebox =
2525
(fastify, _options, done) => {
26+
fastify.get(
27+
'/exam-environment/exams',
28+
{
29+
schema: schemas.examEnvironmentExams
30+
},
31+
getExams
32+
);
2633
fastify.post(
2734
'/exam-environment/exam/generated-exam',
2835
{
@@ -565,3 +572,37 @@ async function postScreenshotHandler(
565572
) {
566573
return reply.code(418);
567574
}
575+
576+
async function getExams(
577+
this: FastifyInstance,
578+
req: UpdateReqType<typeof schemas.examEnvironmentExams>,
579+
reply: FastifyReply
580+
) {
581+
const user = req.user!;
582+
const exams = await this.prisma.envExam.findMany({
583+
select: {
584+
id: true,
585+
config: true
586+
}
587+
});
588+
589+
const availableExams = exams.map(exam => {
590+
const isExamPrerequisitesMet = checkPrerequisites(user, true);
591+
592+
return {
593+
id: exam.id,
594+
config: {
595+
name: exam.config.name,
596+
note: exam.config.note,
597+
totalTimeInMS: exam.config.totalTimeInMS
598+
},
599+
canTake: isExamPrerequisitesMet
600+
};
601+
});
602+
603+
return reply.send({
604+
data: {
605+
exams: availableExams
606+
}
607+
});
608+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Type } from '@fastify/type-provider-typebox';
2+
import { STANDARD_ERROR } from '../utils/errors';
3+
export const examEnvironmentExams = {
4+
headers: Type.Object({
5+
'exam-environment-authorization-token': Type.String()
6+
}),
7+
response: {
8+
200: Type.Union([
9+
Type.Object({
10+
data: Type.Object({
11+
exams: Type.Array(
12+
Type.Object({
13+
id: Type.String(),
14+
config: Type.Object({
15+
name: Type.String(),
16+
note: Type.String(),
17+
totalTimeInMS: Type.Number()
18+
}),
19+
canTake: Type.Boolean()
20+
})
21+
)
22+
})
23+
}),
24+
STANDARD_ERROR
25+
])
26+
}
27+
};

api/src/exam-environment/schemas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { examEnvironmentPostExamAttempt } from './exam-attempt';
22
export { examEnvironmentPostExamGeneratedExam } from './exam-generated-exam';
33
export { examEnvironmentPostScreenshot } from './screenshot';
44
export { examEnvironmentTokenVerify } from './token-verify';
5+
export { examEnvironmentExams } from './exams';

client/i18n/locales/english/intro.json

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,7 +1714,10 @@
17141714
"For this lab, you will create a web page of your favorite recipe."
17151715
]
17161716
},
1717-
"gwyd": { "title": "4", "intro": [] },
1717+
"lecture-html-fundamentals": {
1718+
"title": "HTML Fundamentals",
1719+
"intro": ["Learn about HTML fundamentals in these lecture videos."]
1720+
},
17181721
"lab-travel-agency-page": {
17191722
"title": "Build a Travel Agency Page",
17201723
"intro": [
@@ -1727,7 +1730,10 @@
17271730
"intro": ["For this lab, you will create a video compilation web page."]
17281731
},
17291732
"bzfv": { "title": "8", "intro": [] },
1730-
"snuv": { "title": "9", "intro": [] },
1733+
"review-basic-html": {
1734+
"title": "Basic HTML Review",
1735+
"intro": ["Review the basic HTML topics."]
1736+
},
17311737
"quiz-basic-html": {
17321738
"title": "Basic HTML Quiz",
17331739
"intro": [
@@ -1914,7 +1920,13 @@
19141920
"Test what you've learned in this quiz of how to style forms using CSS."
19151921
]
19161922
},
1917-
"qzcx": { "title": "73", "intro": [] },
1923+
"workshop-rothko-painting": {
1924+
"title": "Design a Rothko Painting",
1925+
"intro": [
1926+
"Every HTML element is its own box – with its own spacing and a border. This is called the Box Model.",
1927+
"In this workshop, you'll use CSS and the Box Model to create your own Rothko-style rectangular art pieces."
1928+
]
1929+
},
19181930
"wozq": { "title": "74", "intro": [] },
19191931
"lab-confidential-email-page": {
19201932
"title": "Build a Confidential Email Page",
@@ -1944,7 +1956,13 @@
19441956
"Test what you've learned in this quiz of using flexbox in CSS."
19451957
]
19461958
},
1947-
"vqut": { "title": "83", "intro": [] },
1959+
"workshop-nutritional-label": {
1960+
"title": "Build a Nutritional Label",
1961+
"intro": [
1962+
"Typography is the art of styling your text to be easily readable and suit its purpose.",
1963+
"In this workshop, you'll use typography to build a nutrition label webpage. You'll practice how to style text, adjust line height, and position your text using CSS."
1964+
]
1965+
},
19481966
"ujcf": { "title": "84", "intro": [] },
19491967
"lab-newspaper-article": {
19501968
"title": "Build a Newspaper Article",
@@ -1998,7 +2016,13 @@
19982016
"Test what you've learned in this quiz of how positioning works in CSS."
19992017
]
20002018
},
2001-
"xebj": { "title": "103", "intro": [] },
2019+
"workshop-piano": {
2020+
"title": "Design a Piano",
2021+
"intro": [
2022+
"Responsive Design tells your webpage how it should look on different-sized screens.",
2023+
"In this workshop, you'll use CSS and Responsive Design to code a piano. You'll also practice media queries and pseudo selectors."
2024+
]
2025+
},
20022026
"jkdt": { "title": "104", "intro": [] },
20032027
"lab-technical-documentation-page": {
20042028
"title": "Build a Technical Documentation Page",
@@ -2013,7 +2037,13 @@
20132037
"Test what you've learned in this quiz of making your webpage responsive."
20142038
]
20152039
},
2016-
"mtbl": { "title": "108", "intro": [] },
2040+
"workshop-city-skyline": {
2041+
"title": "Build a City Skyline",
2042+
"intro": [
2043+
"CSS variables help you organize your styles and reuse them.",
2044+
"In this workshop, you'll build a city skyline. You'll practice how to configure CSS variables so you can reuse them whenever you want."
2045+
]
2046+
},
20172047
"vlov": { "title": "109", "intro": [] },
20182048
"lab-availability-table": {
20192049
"title": "Build an Availability Table",
@@ -2192,7 +2222,12 @@
21922222
"intro": ["Test what you've learned in this quiz on JavaScript Arrays."]
21932223
},
21942224
"dvnt": { "title": "164", "intro": [] },
2195-
"ekdb": { "title": "165", "intro": [] },
2225+
"workshop-recipe-tracker": {
2226+
"title": "Build a Recipe Tracker",
2227+
"intro": [
2228+
"In this workshop, you will review working with JavaScript objects by building a recipe tracker."
2229+
]
2230+
},
21962231
"lab-quiz-game": {
21972232
"title": "Build a Quiz Game",
21982233
"intro": ["For this lab, you will build a quiz game."]

client/i18n/locales/english/translations.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"first-lesson": "Go to the first lesson",
88
"close": "Close",
99
"edit": "Edit",
10+
"copy": "Copy",
1011
"view": "View",
1112
"view-code": "View Code",
1213
"view-project": "View Project",
@@ -1111,6 +1112,16 @@
11111112
"no-thanks": "No thanks, I would like to keep my token",
11121113
"yes-please": "Yes please, I would like to delete my token"
11131114
},
1115+
"exam-token": {
1116+
"exam-token": "Exam Token",
1117+
"note": "Your exam token is a secret key that allows you to access exams. Do not share this token with anyone.",
1118+
"invalidation": "If you generate a new token, your old token will be invalidated.",
1119+
"generate-exam-token": "Generate Exam Token",
1120+
"error": "There was an error generating your token, please try again in a moment.",
1121+
"your-exam-token": "Your Exam Token is: {{token}}",
1122+
"copied": "Token copied to clipboard",
1123+
"copy-error": "Error copying token to clipboard"
1124+
},
11141125
"shortcuts": {
11151126
"title": "Keyboard shortcuts",
11161127
"table-header-action": "Action",

client/src/client-only-routes/show-settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
55
import { createSelector } from 'reselect';
66

77
import { Callout, Container } from '@freecodecamp/ui';
8+
import { useFeatureIsOn } from '@growthbook/growthbook-react';
89

910
import store from 'store';
1011
import envData from '../../config/env.json';
@@ -18,6 +19,7 @@ import Honesty from '../components/settings/honesty';
1819
import Privacy from '../components/settings/privacy';
1920
import { type ThemeProps, Themes } from '../components/settings/theme';
2021
import UserToken from '../components/settings/user-token';
22+
import ExamToken from '../components/settings/exam-token';
2123
import { hardGoTo as navigate } from '../redux/actions';
2224
import {
2325
signInLoadingSelector,
@@ -35,6 +37,7 @@ import {
3537
updateMyKeyboardShortcuts,
3638
verifyCert
3739
} from '../redux/settings/actions';
40+
3841
const { apiLocation } = envData;
3942

4043
// TODO: update types for actions
@@ -126,6 +129,8 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
126129
} = props;
127130
const isSignedInRef = useRef(isSignedIn);
128131

132+
const examTokenFlag = useFeatureIsOn('exam-token-widget');
133+
129134
if (showLoading) {
130135
return <Loader fullScreen={true} />;
131136
}
@@ -172,6 +177,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
172177
<Spacer size='medium' />
173178
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
174179
<Spacer size='medium' />
180+
{examTokenFlag && <ExamToken />}
175181
<Certification
176182
completedChallenges={completedChallenges}
177183
createFlashMessage={createFlashMessage}

0 commit comments

Comments
 (0)