mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-09 10:00:51 -04:00
feat(api): update mobile-login to match api-server (#55863)
This commit is contained in:
committed by
GitHub
parent
36ddf89f9d
commit
838f30e2be
@@ -1,7 +1,30 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { setupServer, superRequest } from '../../jest.utils';
|
||||
import {
|
||||
setupServer,
|
||||
superRequest,
|
||||
createSuperRequest
|
||||
} from '../../jest.utils';
|
||||
import { AUTH0_DOMAIN } from '../utils/env';
|
||||
|
||||
const mockedFetch = jest.fn();
|
||||
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
|
||||
const newUserEmail = 'a.n.random@user.com';
|
||||
|
||||
const mockAuth0NotOk = () => ({
|
||||
ok: false
|
||||
});
|
||||
|
||||
const mockAuth0InvalidEmail = () => ({
|
||||
ok: true,
|
||||
json: () => ({ email: 'invalid-email' })
|
||||
});
|
||||
|
||||
const mockAuth0ValidEmail = () => ({
|
||||
ok: true,
|
||||
json: () => ({ email: newUserEmail })
|
||||
});
|
||||
|
||||
jest.mock('../utils/env', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
@@ -23,4 +46,107 @@ describe('auth0 routes', () => {
|
||||
expect(redirectUrl.pathname).toBe('/authorize');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /mobile-login', () => {
|
||||
let superGet: ReturnType<typeof createSuperRequest>;
|
||||
|
||||
beforeAll(() => {
|
||||
superGet = createSuperRequest({ method: 'GET' });
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.userRateLimit.deleteMany({});
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: { email: newUserEmail }
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rate-limited', async () => {
|
||||
await Promise.all(
|
||||
[...Array(10).keys()].map(() => superGet('/mobile-login'))
|
||||
);
|
||||
|
||||
const res = await superGet('/mobile-login');
|
||||
expect(res.status).toBe(429);
|
||||
});
|
||||
|
||||
it('should return 401 if the authorization header is invalid', async () => {
|
||||
mockedFetch.mockResolvedValueOnce(mockAuth0NotOk());
|
||||
const res = await superGet('/mobile-login').set(
|
||||
'Authorization',
|
||||
'Bearer invalid-token'
|
||||
);
|
||||
|
||||
expect(res.body).toStrictEqual({
|
||||
type: 'danger',
|
||||
message: 'We could not log you in, please try again in a moment.'
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should return 400 if the email is not valid', async () => {
|
||||
mockedFetch.mockResolvedValueOnce(mockAuth0InvalidEmail());
|
||||
const res = await superGet('/mobile-login').set(
|
||||
'Authorization',
|
||||
'Bearer valid-token'
|
||||
);
|
||||
|
||||
expect(res.body).toStrictEqual({
|
||||
type: 'danger',
|
||||
message: 'The email is incorrectly formatted'
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should set the jwt_access_token cookie if the authorization header is valid', async () => {
|
||||
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
|
||||
const res = await superGet('/mobile-login').set(
|
||||
'Authorization',
|
||||
'Bearer valid-token'
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.get('Set-Cookie')).toEqual(
|
||||
expect.arrayContaining([expect.stringMatching(/jwt_access_token=/)])
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a user if they do not exist', async () => {
|
||||
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
|
||||
const existingUserCount = await fastifyTestInstance.prisma.user.count();
|
||||
|
||||
const res = await superGet('/mobile-login').set(
|
||||
'Authorization',
|
||||
'Bearer valid-token'
|
||||
);
|
||||
|
||||
const newUserCount = await fastifyTestInstance.prisma.user.count();
|
||||
|
||||
expect(existingUserCount).toBe(0);
|
||||
expect(newUserCount).toBe(1);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should redirect to returnTo if already logged in', async () => {
|
||||
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
|
||||
const firstRes = await superGet('/mobile-login').set(
|
||||
'Authorization',
|
||||
'Bearer valid-token'
|
||||
);
|
||||
|
||||
expect(firstRes.status).toBe(200);
|
||||
|
||||
const res = await superRequest('/mobile-login', {
|
||||
method: 'GET',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
setCookies: firstRes.get('Set-Cookie')
|
||||
})
|
||||
.set('Authorization', 'Bearer does-not-matter')
|
||||
.set('Referer', 'https://www.freecodecamp.org/back-home');
|
||||
|
||||
expect(res.status).toBe(302);
|
||||
expect(res.headers.location).toBe(
|
||||
'https://www.freecodecamp.org/back-home'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,25 +3,28 @@ import { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
// @ts-expect-error - no types
|
||||
import MongoStoreRL from 'rate-limit-mongo';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import { AUTH0_DOMAIN, MONGOHQ_URL } from '../utils/env';
|
||||
import { auth0Client } from '../plugins/auth0';
|
||||
import { createAccessToken } from '../utils/tokens';
|
||||
import { findOrCreateUser } from './helpers/auth-helpers';
|
||||
|
||||
const getEmailFromAuth0 = async (req: FastifyRequest) => {
|
||||
const getEmailFromAuth0 = async (
|
||||
req: FastifyRequest
|
||||
): Promise<string | null> => {
|
||||
const auth0Res = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
|
||||
headers: {
|
||||
Authorization: req.headers.authorization ?? ''
|
||||
}
|
||||
});
|
||||
|
||||
if (!auth0Res.ok) {
|
||||
req.log.error(auth0Res);
|
||||
throw new Error('Invalid Auth0 Access Token');
|
||||
}
|
||||
if (!auth0Res.ok) return null;
|
||||
|
||||
const { email } = (await auth0Res.json()) as { email: string };
|
||||
return email;
|
||||
// For now, we assume the response is a JSON object. If not, we can't proceed
|
||||
// and the only safe thing to do is to throw.
|
||||
const { email } = (await auth0Res.json()) as { email?: string };
|
||||
return typeof email === 'string' ? email : null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -60,10 +63,25 @@ export const mobileAuth0Routes: FastifyPluginCallback = (
|
||||
// all auth routes.
|
||||
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
|
||||
|
||||
fastify.get('/mobile-login', async req => {
|
||||
fastify.get('/mobile-login', async (req, reply) => {
|
||||
const email = await getEmailFromAuth0(req);
|
||||
|
||||
await findOrCreateUser(fastify, email);
|
||||
if (!email) {
|
||||
return reply.status(401).send({
|
||||
message: 'We could not log you in, please try again in a moment.',
|
||||
type: 'danger'
|
||||
});
|
||||
}
|
||||
if (!isEmail(email)) {
|
||||
return reply.status(400).send({
|
||||
message: 'The email is incorrectly formatted',
|
||||
type: 'danger'
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = await findOrCreateUser(fastify, email);
|
||||
|
||||
reply.setAccessTokenCookie(createAccessToken(id));
|
||||
});
|
||||
|
||||
done();
|
||||
|
||||
Reference in New Issue
Block a user