Skip to content

Commit bc30d68

Browse files
Merge pull request #713 from freeCodeCamp/main
Create a new pull request by comparing changes across two branches
2 parents d2c06e4 + 715eeca commit bc30d68

File tree

133 files changed

+1525
-749
lines changed

Some content is hidden

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

133 files changed

+1525
-749
lines changed

api/src/routes/auth.test.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2-
import { setupServer, superRequest } from '../../jest.utils';
2+
import {
3+
setupServer,
4+
superRequest,
5+
createSuperRequest
6+
} from '../../jest.utils';
37
import { AUTH0_DOMAIN } from '../utils/env';
48

9+
const mockedFetch = jest.fn();
10+
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
11+
12+
const newUserEmail = '[email protected]';
13+
14+
const mockAuth0NotOk = () => ({
15+
ok: false
16+
});
17+
18+
const mockAuth0InvalidEmail = () => ({
19+
ok: true,
20+
json: () => ({ email: 'invalid-email' })
21+
});
22+
23+
const mockAuth0ValidEmail = () => ({
24+
ok: true,
25+
json: () => ({ email: newUserEmail })
26+
});
27+
528
jest.mock('../utils/env', () => {
629
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
730
return {
@@ -23,4 +46,107 @@ describe('auth0 routes', () => {
2346
expect(redirectUrl.pathname).toBe('/authorize');
2447
});
2548
});
49+
50+
describe('GET /mobile-login', () => {
51+
let superGet: ReturnType<typeof createSuperRequest>;
52+
53+
beforeAll(() => {
54+
superGet = createSuperRequest({ method: 'GET' });
55+
});
56+
beforeEach(async () => {
57+
await fastifyTestInstance.prisma.userRateLimit.deleteMany({});
58+
await fastifyTestInstance.prisma.user.deleteMany({
59+
where: { email: newUserEmail }
60+
});
61+
});
62+
63+
it('should be rate-limited', async () => {
64+
await Promise.all(
65+
[...Array(10).keys()].map(() => superGet('/mobile-login'))
66+
);
67+
68+
const res = await superGet('/mobile-login');
69+
expect(res.status).toBe(429);
70+
});
71+
72+
it('should return 401 if the authorization header is invalid', async () => {
73+
mockedFetch.mockResolvedValueOnce(mockAuth0NotOk());
74+
const res = await superGet('/mobile-login').set(
75+
'Authorization',
76+
'Bearer invalid-token'
77+
);
78+
79+
expect(res.body).toStrictEqual({
80+
type: 'danger',
81+
message: 'We could not log you in, please try again in a moment.'
82+
});
83+
expect(res.status).toBe(401);
84+
});
85+
86+
it('should return 400 if the email is not valid', async () => {
87+
mockedFetch.mockResolvedValueOnce(mockAuth0InvalidEmail());
88+
const res = await superGet('/mobile-login').set(
89+
'Authorization',
90+
'Bearer valid-token'
91+
);
92+
93+
expect(res.body).toStrictEqual({
94+
type: 'danger',
95+
message: 'The email is incorrectly formatted'
96+
});
97+
expect(res.status).toBe(400);
98+
});
99+
100+
it('should set the jwt_access_token cookie if the authorization header is valid', async () => {
101+
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
102+
const res = await superGet('/mobile-login').set(
103+
'Authorization',
104+
'Bearer valid-token'
105+
);
106+
107+
expect(res.status).toBe(200);
108+
expect(res.get('Set-Cookie')).toEqual(
109+
expect.arrayContaining([expect.stringMatching(/jwt_access_token=/)])
110+
);
111+
});
112+
113+
it('should create a user if they do not exist', async () => {
114+
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
115+
const existingUserCount = await fastifyTestInstance.prisma.user.count();
116+
117+
const res = await superGet('/mobile-login').set(
118+
'Authorization',
119+
'Bearer valid-token'
120+
);
121+
122+
const newUserCount = await fastifyTestInstance.prisma.user.count();
123+
124+
expect(existingUserCount).toBe(0);
125+
expect(newUserCount).toBe(1);
126+
expect(res.status).toBe(200);
127+
});
128+
129+
it('should redirect to returnTo if already logged in', async () => {
130+
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
131+
const firstRes = await superGet('/mobile-login').set(
132+
'Authorization',
133+
'Bearer valid-token'
134+
);
135+
136+
expect(firstRes.status).toBe(200);
137+
138+
const res = await superRequest('/mobile-login', {
139+
method: 'GET',
140+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
141+
setCookies: firstRes.get('Set-Cookie')
142+
})
143+
.set('Authorization', 'Bearer does-not-matter')
144+
.set('Referer', 'https://www.freecodecamp.org/back-home');
145+
146+
expect(res.status).toBe(302);
147+
expect(res.headers.location).toBe(
148+
'https://www.freecodecamp.org/back-home'
149+
);
150+
});
151+
});
26152
});

api/src/routes/auth.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,28 @@ import { FastifyPluginCallback, FastifyRequest } from 'fastify';
33
import rateLimit from 'express-rate-limit';
44
// @ts-expect-error - no types
55
import MongoStoreRL from 'rate-limit-mongo';
6+
import isEmail from 'validator/lib/isEmail';
67

78
import { AUTH0_DOMAIN, MONGOHQ_URL } from '../utils/env';
89
import { auth0Client } from '../plugins/auth0';
10+
import { createAccessToken } from '../utils/tokens';
911
import { findOrCreateUser } from './helpers/auth-helpers';
1012

11-
const getEmailFromAuth0 = async (req: FastifyRequest) => {
13+
const getEmailFromAuth0 = async (
14+
req: FastifyRequest
15+
): Promise<string | null> => {
1216
const auth0Res = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
1317
headers: {
1418
Authorization: req.headers.authorization ?? ''
1519
}
1620
});
1721

18-
if (!auth0Res.ok) {
19-
req.log.error(auth0Res);
20-
throw new Error('Invalid Auth0 Access Token');
21-
}
22+
if (!auth0Res.ok) return null;
2223

23-
const { email } = (await auth0Res.json()) as { email: string };
24-
return email;
24+
// For now, we assume the response is a JSON object. If not, we can't proceed
25+
// and the only safe thing to do is to throw.
26+
const { email } = (await auth0Res.json()) as { email?: string };
27+
return typeof email === 'string' ? email : null;
2528
};
2629

2730
/**
@@ -60,10 +63,25 @@ export const mobileAuth0Routes: FastifyPluginCallback = (
6063
// all auth routes.
6164
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
6265

63-
fastify.get('/mobile-login', async req => {
66+
fastify.get('/mobile-login', async (req, reply) => {
6467
const email = await getEmailFromAuth0(req);
6568

66-
await findOrCreateUser(fastify, email);
69+
if (!email) {
70+
return reply.status(401).send({
71+
message: 'We could not log you in, please try again in a moment.',
72+
type: 'danger'
73+
});
74+
}
75+
if (!isEmail(email)) {
76+
return reply.status(400).send({
77+
message: 'The email is incorrectly formatted',
78+
type: 'danger'
79+
});
80+
}
81+
82+
const { id } = await findOrCreateUser(fastify, email);
83+
84+
reply.setAccessTokenCookie(createAccessToken(id));
6785
});
6886

6987
done();

client/i18n/locales/arabic/intro.json

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,10 +2289,6 @@
22892289
"title": "139",
22902290
"intro": []
22912291
},
2292-
"uzjg": {
2293-
"title": "140",
2294-
"intro": []
2295-
},
22962292
"hxwa": {
22972293
"title": "141",
22982294
"intro": []
@@ -2301,9 +2297,11 @@
23012297
"title": "142",
23022298
"intro": []
23032299
},
2304-
"nkxq": {
2305-
"title": "143",
2306-
"intro": []
2300+
"lab-fortune-teller": {
2301+
"title": "Build a Fortune Teller",
2302+
"intro": [
2303+
"In this lab, you will build a fortune teller by randomly selecting a fortune from the avaialble fortunes."
2304+
]
23072305
},
23082306
"wfyg": {
23092307
"title": "144",
@@ -2343,13 +2341,17 @@
23432341
"In this workshop, you will review working with functions by building a calculator."
23442342
]
23452343
},
2346-
"hqba": {
2347-
"title": "153",
2348-
"intro": []
2344+
"lab-email-masker": {
2345+
"title": "Build an Email Masker",
2346+
"intro": [
2347+
"In this lab, you'll build an email masker that will take an email address and obscure it."
2348+
]
23492349
},
2350-
"nnqa": {
2351-
"title": "154",
2352-
"intro": []
2350+
"lab-sentence-maker": {
2351+
"title": "Build a Sentence Maker",
2352+
"intro": [
2353+
"In this lab, you will create different stories by assigning different words to different variables."
2354+
]
23532355
},
23542356
"cktz": {
23552357
"title": "155",

client/i18n/locales/chinese-traditional/intro.json

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,10 +2275,6 @@
22752275
"title": "139",
22762276
"intro": []
22772277
},
2278-
"uzjg": {
2279-
"title": "140",
2280-
"intro": []
2281-
},
22822278
"hxwa": {
22832279
"title": "141",
22842280
"intro": []
@@ -2287,9 +2283,11 @@
22872283
"title": "142",
22882284
"intro": []
22892285
},
2290-
"nkxq": {
2291-
"title": "143",
2292-
"intro": []
2286+
"lab-fortune-teller": {
2287+
"title": "Build a Fortune Teller",
2288+
"intro": [
2289+
"In this lab, you will build a fortune teller by randomly selecting a fortune from the avaialble fortunes."
2290+
]
22932291
},
22942292
"wfyg": {
22952293
"title": "144",
@@ -2329,13 +2327,17 @@
23292327
"In this workshop, you will review working with functions by building a calculator."
23302328
]
23312329
},
2332-
"hqba": {
2333-
"title": "153",
2334-
"intro": []
2330+
"lab-email-masker": {
2331+
"title": "Build an Email Masker",
2332+
"intro": [
2333+
"In this lab, you'll build an email masker that will take an email address and obscure it."
2334+
]
23352335
},
2336-
"nnqa": {
2337-
"title": "154",
2338-
"intro": []
2336+
"lab-sentence-maker": {
2337+
"title": "Build a Sentence Maker",
2338+
"intro": [
2339+
"In this lab, you will create different stories by assigning different words to different variables."
2340+
]
23392341
},
23402342
"cktz": {
23412343
"title": "155",

client/i18n/locales/chinese/intro.json

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,10 +2275,6 @@
22752275
"title": "139",
22762276
"intro": []
22772277
},
2278-
"uzjg": {
2279-
"title": "140",
2280-
"intro": []
2281-
},
22822278
"hxwa": {
22832279
"title": "141",
22842280
"intro": []
@@ -2287,9 +2283,11 @@
22872283
"title": "142",
22882284
"intro": []
22892285
},
2290-
"nkxq": {
2291-
"title": "143",
2292-
"intro": []
2286+
"lab-fortune-teller": {
2287+
"title": "Build a Fortune Teller",
2288+
"intro": [
2289+
"In this lab, you will build a fortune teller by randomly selecting a fortune from the avaialble fortunes."
2290+
]
22932291
},
22942292
"wfyg": {
22952293
"title": "144",
@@ -2329,13 +2327,17 @@
23292327
"In this workshop, you will review working with functions by building a calculator."
23302328
]
23312329
},
2332-
"hqba": {
2333-
"title": "153",
2334-
"intro": []
2330+
"lab-email-masker": {
2331+
"title": "Build an Email Masker",
2332+
"intro": [
2333+
"In this lab, you'll build an email masker that will take an email address and obscure it."
2334+
]
23352335
},
2336-
"nnqa": {
2337-
"title": "154",
2338-
"intro": []
2336+
"lab-sentence-maker": {
2337+
"title": "Build a Sentence Maker",
2338+
"intro": [
2339+
"In this lab, you will create different stories by assigning different words to different variables."
2340+
]
23392341
},
23402342
"cktz": {
23412343
"title": "155",

client/i18n/locales/english/intro.json

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,26 +1917,39 @@
19171917
},
19181918
"axgb": { "title": "138", "intro": [] },
19191919
"rwac": { "title": "139", "intro": [] },
1920-
"uzjg": { "title": "140", "intro": [] },
19211920
"hxwa": { "title": "141", "intro": [] },
19221921
"xkbo": { "title": "142", "intro": [] },
1923-
"nkxq": { "title": "143", "intro": [] },
1922+
"lab-fortune-teller": {
1923+
"title": "Build a Fortune Teller",
1924+
"intro": [
1925+
"In this lab, you will build a fortune teller by randomly selecting a fortune from the avaialble fortunes."
1926+
]
1927+
},
19241928
"wfyg": { "title": "144", "intro": [] },
19251929
"guqy": { "title": "145", "intro": [] },
19261930
"nkge": { "title": "146", "intro": [] },
19271931
"hafd": { "title": "147", "intro": [] },
19281932
"phko": { "title": "148", "intro": [] },
19291933
"wjzt": { "title": "149", "intro": [] },
19301934
"tsdq": { "title": "150", "intro": [] },
1931-
"kkix": { "title": "151", "intro": [] },
19321935
"workshop-calculator": {
19331936
"title": "Build a Calculator",
19341937
"intro": [
19351938
"In this workshop, you will review working with functions by building a calculator."
19361939
]
19371940
},
1938-
"hqba": { "title": "153", "intro": [] },
1939-
"nnqa": { "title": "154", "intro": [] },
1941+
"lab-email-masker": {
1942+
"title": "Build an Email Masker",
1943+
"intro": [
1944+
"In this lab, you'll build an email masker that will take an email address and obscure it."
1945+
]
1946+
},
1947+
"lab-sentence-maker": {
1948+
"title": "Build a Sentence Maker",
1949+
"intro": [
1950+
"In this lab, you will create different stories by assigning different words to different variables."
1951+
]
1952+
},
19401953
"cktz": { "title": "155", "intro": [] },
19411954
"pljo": { "title": "156", "intro": [] },
19421955
"avln": { "title": "157", "intro": [] },

0 commit comments

Comments
 (0)