feat(api): dev login (#49880)

Co-authored-by: Mrugesh Mohapatra <hi@mrugesh.dev>
This commit is contained in:
Oliver Eyton-Williams
2023-03-29 14:38:38 +02:00
committed by GitHub
parent 93192539e5
commit 06d4076a45
7 changed files with 94 additions and 49 deletions

View File

@@ -1,18 +1,24 @@
# Connecting to local database
# Working on the new api
## Connecting to local database
The api uses the ORM Prisma and it needs the MongoDB instance to be a replica set.
## Atlas
### Atlas
If you use MongoDB Atlas, the set is managed for you.
## Local
### Local
The simplest way to run a replica set locally is to use the docker-compose file
in /tools. First disable any running MongoDB instance on your machin, then run
in /tools. First disable any running MongoDB instance on your machine, then run
the docker-compose file.
```bash
cd tools
docker compose up -d
```
## Login in development/testing
During development and testing, the api exposes the endpoint GET auth/dev-callback. Calling this will log you in as the user with the email `foo@bar.com` by setting the session cookie for that user.

View File

@@ -38,7 +38,7 @@
"name": "@freecodecamp/api",
"nodemonConfig": {
"env": {
"NODE_ENV": "development"
"FREECODECAMP_NODE_ENV": "development"
},
"ignore": [
"**/*.js"
@@ -53,7 +53,7 @@
"build": "tsc -p tsconfig.build.json",
"clean": "rm -rf dist",
"develop": "nodemon src/server.ts",
"start": "NODE_ENV=production node dist/server.js",
"start": "FREECODECAMP_NODE_ENV=production node dist/server.js",
"test": "jest --force-exit",
"prisma": "MONGOHQ_URL=mongodb://localhost:27017/freecodecamp?directConnection=true prisma",
"postinstall": "prisma generate"

View File

@@ -18,7 +18,7 @@ import fastifySwaggerUI from '@fastify/swagger-ui';
import jwtAuthz from './plugins/fastify-jwt-authz';
import sessionAuth from './plugins/session-auth';
import { testRoutes } from './routes/test';
import { auth0Routes } from './routes/auth0';
import { auth0Routes, devLoginCallback } from './routes/auth';
import { testValidatedRoutes } from './routes/validation-test';
import { testMiddleware } from './middleware';
import prismaPlugin from './db/prisma';
@@ -26,11 +26,12 @@ import prismaPlugin from './db/prisma';
import {
AUTH0_AUDIENCE,
AUTH0_DOMAIN,
NODE_ENV,
FREECODECAMP_NODE_ENV,
MONGOHQ_URL,
SESSION_SECRET,
FCC_ENABLE_SWAGGER_UI,
API_LOCATION
API_LOCATION,
FCC_ENABLE_DEV_LOGIN_MODE
} from './utils/env';
export type FastifyInstanceWithTypeProvider = FastifyInstance<
@@ -60,7 +61,7 @@ export const build = async (
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60, // 1 hour
secure: NODE_ENV !== 'development'
secure: FREECODECAMP_NODE_ENV !== 'development'
},
store: MongoStore.create({
mongoUrl: MONGOHQ_URL
@@ -104,7 +105,10 @@ export const build = async (
void fastify.register(prismaPlugin);
void fastify.register(testRoutes);
void fastify.register(auth0Routes, { prefix: '/auth0' });
void fastify.register(auth0Routes, { prefix: '/auth' });
if (FCC_ENABLE_DEV_LOGIN_MODE) {
void fastify.register(devLoginCallback, { prefix: '/auth' });
}
void fastify.register(testValidatedRoutes);
return fastify;
};

View File

@@ -1,4 +1,8 @@
import { FastifyPluginCallback } from 'fastify';
import {
FastifyInstance,
FastifyPluginCallback,
FastifyRequest
} from 'fastify';
import { AUTH0_DOMAIN } from '../utils/env';
@@ -65,39 +69,64 @@ const defaultUser = {
username: ''
};
const getEmailFromAuth0 = async (req: FastifyRequest) => {
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');
}
const { email } = (await auth0Res.json()) as { email: string };
return email;
};
const findOrCreateUser = async (fastify: FastifyInstance, email: string) => {
// TODO: handle the case where there are multiple users with the same email.
// e.g. use findMany and throw an error if more than one is found.
const existingUser = await fastify.prisma.user.findFirst({
where: { email },
select: { id: true }
});
return (
existingUser ??
(await fastify.prisma.user.create({
data: { ...defaultUser, email },
select: { id: true }
}))
);
};
export const devLoginCallback: FastifyPluginCallback = (
fastify,
_options,
done
) => {
fastify.get('/dev-callback', async (req, _res) => {
const email = 'foo@bar.com';
const { id } = await findOrCreateUser(fastify, email);
req.session.user = { id };
await req.session.save();
});
done();
};
export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => {
fastify.addHook('onRequest', fastify.authenticate);
fastify.get('/callback', async (req, _res) => {
const auth0Res = await fetch(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`https://${AUTH0_DOMAIN}/userinfo`,
{
headers: {
Authorization: req.headers.authorization ?? ''
}
}
);
const email = await getEmailFromAuth0(req);
if (!auth0Res.ok) {
fastify.log.error(auth0Res);
throw new Error('Invalid Auth0 Access Token');
}
const { email } = (await auth0Res.json()) as { email: string };
const existingUser = await fastify.prisma.user.findFirst({
where: { email }
});
if (existingUser) {
req.session.user = { id: existingUser.id };
} else {
const newUser = await fastify.prisma.user.create({
data: { ...defaultUser, email }
});
req.session.user = { id: newUser.id };
}
const { id } = await findOrCreateUser(fastify, email);
req.session.user = { id };
await req.session.save();
});
done();
};

View File

@@ -1,6 +1,6 @@
import { build } from './app';
import { NODE_ENV, PORT } from './utils/env';
import { FREECODECAMP_NODE_ENV, PORT } from './utils/env';
const envToLogger = {
development: {
@@ -19,7 +19,7 @@ const envToLogger = {
};
const start = async () => {
const fastify = await build({ logger: envToLogger[NODE_ENV] });
const fastify = await build({ logger: envToLogger[FREECODECAMP_NODE_ENV] });
try {
const port = Number(PORT);
fastify.log.info(`Starting server on port ${port}`);

View File

@@ -18,21 +18,20 @@ if (error) {
`);
}
function isAllowedEnv(
env: string
): env is 'development' | 'production' | 'test' {
return ['development', 'production', 'test'].includes(env);
function isAllowedEnv(env: string): env is 'development' | 'production' {
return ['development', 'production'].includes(env);
}
assert.ok(process.env.NODE_ENV);
assert.ok(isAllowedEnv(process.env.NODE_ENV));
assert.ok(process.env.FREECODECAMP_NODE_ENV);
assert.ok(isAllowedEnv(process.env.FREECODECAMP_NODE_ENV));
assert.ok(process.env.AUTH0_DOMAIN);
assert.ok(process.env.AUTH0_AUDIENCE);
assert.ok(process.env.API_LOCATION);
assert.ok(process.env.SESSION_SECRET);
assert.ok(process.env.FCC_ENABLE_SWAGGER_UI);
assert.ok(process.env.FCC_ENABLE_DEV_LOGIN_MODE);
if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test') {
if (process.env.FREECODECAMP_NODE_ENV !== 'development') {
assert.ok(process.env.PORT);
assert.ok(process.env.MONGOHQ_URL);
assert.notEqual(
@@ -40,12 +39,16 @@ if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test') {
'a_thirty_two_plus_character_session_secret',
'The session secret should be changed from the default value.'
);
assert.ok(
process.env.FCC_ENABLE_DEV_LOGIN_MODE !== 'true',
'Dev login mode MUST be disabled in production.'
);
}
export const MONGOHQ_URL =
process.env.MONGOHQ_URL ??
'mongodb://localhost:27017/freecodecamp?directConnection=true';
export const NODE_ENV = process.env.NODE_ENV;
export const FREECODECAMP_NODE_ENV = process.env.FREECODECAMP_NODE_ENV;
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
export const PORT = process.env.PORT || '3000';
@@ -53,3 +56,5 @@ export const API_LOCATION = process.env.API_LOCATION;
export const SESSION_SECRET = process.env.SESSION_SECRET;
export const FCC_ENABLE_SWAGGER_UI =
process.env.FCC_ENABLE_SWAGGER_UI === 'true';
export const FCC_ENABLE_DEV_LOGIN_MODE =
process.env.FCC_ENABLE_DEV_LOGIN_MODE === 'true';

View File

@@ -76,3 +76,4 @@ CODESEE=false
NODE_ENV=development
PORT=3000
FCC_ENABLE_SWAGGER_UI=true
FCC_ENABLE_DEV_LOGIN_MODE=true