Skip to content

Commit

Permalink
feat(api): dev login (#49880)
Browse files Browse the repository at this point in the history
Co-authored-by: Mrugesh Mohapatra <hi@mrugesh.dev>
  • Loading branch information
ojeytonwilliams and raisedadead committed Mar 29, 2023
1 parent 9319253 commit 06d4076
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 49 deletions.
14 changes: 10 additions & 4 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"name": "@freecodecamp/api",
"nodemonConfig": {
"env": {
"NODE_ENV": "development"
"FREECODECAMP_NODE_ENV": "development"
},
"ignore": [
"**/*.js"
Expand All @@ -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"
Expand Down
14 changes: 9 additions & 5 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@ 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';

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<
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
};
85 changes: 57 additions & 28 deletions api/src/routes/auth0.ts → api/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { FastifyPluginCallback } from 'fastify';
import {
FastifyInstance,
FastifyPluginCallback,
FastifyRequest
} from 'fastify';

import { AUTH0_DOMAIN } from '../utils/env';

Expand Down Expand Up @@ -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();
};
4 changes: 2 additions & 2 deletions api/src/server.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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}`);
Expand Down
21 changes: 13 additions & 8 deletions api/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,43 @@ 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(
process.env.SESSION_SECRET,
'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';
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';
1 change: 1 addition & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ CODESEE=false
NODE_ENV=development
PORT=3000
FCC_ENABLE_SWAGGER_UI=true
FCC_ENABLE_DEV_LOGIN_MODE=true

0 comments on commit 06d4076

Please sign in to comment.