Skip to content

Commit 838f30e

Browse files
feat(api): update mobile-login to match api-server (freeCodeCamp#55863)
1 parent 36ddf89 commit 838f30e

File tree

2 files changed

+154
-10
lines changed

2 files changed

+154
-10
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();

0 commit comments

Comments
 (0)