mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): sessions management (#49499)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
+29
-5
@@ -1,12 +1,24 @@
|
|||||||
import fastifyAuth0 from 'fastify-auth0-verify';
|
import fastifyAuth0 from 'fastify-auth0-verify';
|
||||||
import Fastify from 'fastify';
|
import Fastify from 'fastify';
|
||||||
import middie from '@fastify/middie';
|
import middie from '@fastify/middie';
|
||||||
|
import fastifySession from '@fastify/session';
|
||||||
|
import fastifyCookie from '@fastify/cookie';
|
||||||
|
import MongoStore from 'connect-mongo';
|
||||||
|
|
||||||
import jwtAuthz from './plugins/fastify-jwt-authz';
|
import jwtAuthz from './plugins/fastify-jwt-authz';
|
||||||
|
import sessionAuth from './plugins/session-auth';
|
||||||
import { testRoutes } from './routes/test';
|
import { testRoutes } from './routes/test';
|
||||||
|
import { auth0Routes } from './routes/auth0';
|
||||||
import { dbConnector } from './db';
|
import { dbConnector } from './db';
|
||||||
import { auth0Verify, testMiddleware } from './middleware';
|
import { testMiddleware } from './middleware';
|
||||||
import { AUTH0_AUDIENCE, AUTH0_DOMAIN, NODE_ENV, PORT } from './utils/env';
|
import {
|
||||||
|
AUTH0_AUDIENCE,
|
||||||
|
AUTH0_DOMAIN,
|
||||||
|
NODE_ENV,
|
||||||
|
PORT,
|
||||||
|
MONGOHQ_URL,
|
||||||
|
SESSION_SECRET
|
||||||
|
} from './utils/env';
|
||||||
|
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' }
|
logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' }
|
||||||
@@ -19,6 +31,19 @@ fastify.get('/', async (_request, _reply) => {
|
|||||||
const start = async () => {
|
const start = async () => {
|
||||||
// NOTE: Awaited to ensure `.use` is registered on `fastify`
|
// NOTE: Awaited to ensure `.use` is registered on `fastify`
|
||||||
await fastify.register(middie);
|
await fastify.register(middie);
|
||||||
|
await fastify.register(fastifyCookie);
|
||||||
|
await fastify.register(fastifySession, {
|
||||||
|
secret: SESSION_SECRET,
|
||||||
|
rolling: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1000 * 60 * 60, // 1 hour
|
||||||
|
secure: NODE_ENV !== 'development'
|
||||||
|
},
|
||||||
|
store: MongoStore.create({
|
||||||
|
mongoUrl: MONGOHQ_URL
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// Auth0 plugin
|
// Auth0 plugin
|
||||||
void fastify.register(fastifyAuth0, {
|
void fastify.register(fastifyAuth0, {
|
||||||
@@ -26,14 +51,13 @@ const start = async () => {
|
|||||||
audience: AUTH0_AUDIENCE
|
audience: AUTH0_AUDIENCE
|
||||||
});
|
});
|
||||||
void fastify.register(jwtAuthz);
|
void fastify.register(jwtAuthz);
|
||||||
|
void fastify.register(sessionAuth);
|
||||||
|
|
||||||
void fastify.use('/test', testMiddleware);
|
void fastify.use('/test', testMiddleware);
|
||||||
|
|
||||||
// Hooks
|
|
||||||
void fastify.addHook('preValidation', auth0Verify);
|
|
||||||
|
|
||||||
void fastify.register(dbConnector);
|
void fastify.register(dbConnector);
|
||||||
void fastify.register(testRoutes);
|
void fastify.register(testRoutes);
|
||||||
|
void fastify.register(auth0Routes, { prefix: '/auth0' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const port = Number(PORT);
|
const port = Number(PORT);
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
||||||
import type { NextFunction, NextHandleFunction } from '@fastify/middie';
|
import type { NextFunction, NextHandleFunction } from '@fastify/middie';
|
||||||
|
|
||||||
export async function auth0Verify(
|
|
||||||
this: FastifyInstance,
|
|
||||||
request: FastifyRequest,
|
|
||||||
reply: FastifyReply
|
|
||||||
): Promise<void> {
|
|
||||||
await this.authenticate(request, reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
type MiddieRequest = Parameters<NextHandleFunction>[0];
|
type MiddieRequest = Parameters<NextHandleFunction>[0];
|
||||||
type MiddieResponse = Parameters<NextHandleFunction>[1];
|
type MiddieResponse = Parameters<NextHandleFunction>[1];
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^8.3.0",
|
||||||
"@fastify/middie": "8.1",
|
"@fastify/middie": "8.1",
|
||||||
"@fastify/mongodb": "6.2.0",
|
"@fastify/mongodb": "6.2.0",
|
||||||
|
"@fastify/session": "^10.1.1",
|
||||||
|
"connect-mongo": "^4.6.0",
|
||||||
"fastify": "4.14.0",
|
"fastify": "4.14.0",
|
||||||
"fastify-auth0-verify": "^1.0.0",
|
"fastify-auth0-verify": "^1.0.0",
|
||||||
"fastify-plugin": "^4.3.0",
|
"fastify-plugin": "^4.3.0",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ SOFTWARE.
|
|||||||
import { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
import { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||||
import fp from 'fastify-plugin';
|
import fp from 'fastify-plugin';
|
||||||
|
|
||||||
interface UserObject {
|
export interface UserObject {
|
||||||
scope?: string;
|
scope?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { FastifyPluginCallback, onRequestHookHandler } from 'fastify';
|
||||||
|
import fp from 'fastify-plugin';
|
||||||
|
|
||||||
|
const sessionAuth: FastifyPluginCallback = (fastify, _opts, done) => {
|
||||||
|
const authenticateSession: onRequestHookHandler = (req, res, done) => {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.statusCode = 401;
|
||||||
|
void res.send({ msg: 'Unauthorized' });
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fastify.decorate('authenticateSession', authenticateSession);
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyInstance {
|
||||||
|
authenticateSession: onRequestHookHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fp(sessionAuth);
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { FastifyPluginCallback } from 'fastify';
|
||||||
|
|
||||||
|
import { AUTH0_DOMAIN } from '../utils/env';
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||||
|
fastify.addHook('onRequest', fastify.authenticate);
|
||||||
|
const collection = fastify.mongo.db?.collection('user');
|
||||||
|
|
||||||
|
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 ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!auth0Res.ok) {
|
||||||
|
fastify.log.error(auth0Res);
|
||||||
|
throw new Error('Invalid Auth0 Access Token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email } = (await auth0Res.json()) as { email: string };
|
||||||
|
const user = await collection?.findOne({ email });
|
||||||
|
if (user) {
|
||||||
|
req.session.user = { id: user._id.toString() };
|
||||||
|
} else {
|
||||||
|
const DBRes = await collection?.insertOne({ email });
|
||||||
|
req.session.user = { id: DBRes?.insertedId.toString() ?? '' };
|
||||||
|
}
|
||||||
|
await req.session.save();
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
};
|
||||||
+12
-14
@@ -1,30 +1,26 @@
|
|||||||
import { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
import { ObjectId } from '@fastify/mongodb';
|
||||||
|
import { FastifyPluginCallback } from 'fastify';
|
||||||
|
|
||||||
export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
||||||
const collection = fastify.mongo.db?.collection('user');
|
const collection = fastify.mongo.db?.collection('user');
|
||||||
|
|
||||||
fastify.get('/test', async (_request, _reply) => {
|
fastify.addHook('onRequest', fastify.authenticateSession);
|
||||||
|
|
||||||
|
fastify.get('/test', async (request, _reply) => {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return { error: 'No collection' };
|
return { error: 'No collection' };
|
||||||
}
|
}
|
||||||
const user = await collection?.findOne({ email: 'bar@bar.com' });
|
const userId = new ObjectId(request.session.user.id);
|
||||||
|
const user = await collection?.findOne({ _id: userId });
|
||||||
return { user };
|
return { user };
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.put(
|
fastify.put<{ Body: { quincyEmails: boolean } }>(
|
||||||
'/update-privacy-terms',
|
'/update-privacy-terms',
|
||||||
{
|
{
|
||||||
preHandler: [
|
|
||||||
function (
|
|
||||||
req: FastifyRequest<{ Body: { quincyEmails: boolean } }>,
|
|
||||||
_res,
|
|
||||||
done
|
|
||||||
) {
|
|
||||||
void req.jwtAuthz(['write:user'], done);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
schema: {
|
schema: {
|
||||||
body: {
|
body: {
|
||||||
|
type: 'object',
|
||||||
required: ['quincyEmails'],
|
required: ['quincyEmails'],
|
||||||
properties: {
|
properties: {
|
||||||
quincyEmails: { type: 'boolean' }
|
quincyEmails: { type: 'boolean' }
|
||||||
@@ -42,8 +38,10 @@ export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
|
|||||||
sendQuincyEmail: !!quincyEmails
|
sendQuincyEmail: !!quincyEmails
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userId = new ObjectId(req.session.user.id);
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
?.updateOne({ email: 'bar@bar.com' }, { $set: update })
|
?.updateOne({ _id: userId }, { $set: update })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
void res.code(200).send({ msg: 'Successfully updated' });
|
void res.code(200).send({ msg: 'Successfully updated' });
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ if (error) {
|
|||||||
assert.ok(process.env.NODE_ENV);
|
assert.ok(process.env.NODE_ENV);
|
||||||
assert.ok(process.env.AUTH0_DOMAIN);
|
assert.ok(process.env.AUTH0_DOMAIN);
|
||||||
assert.ok(process.env.AUTH0_AUDIENCE);
|
assert.ok(process.env.AUTH0_AUDIENCE);
|
||||||
|
assert.ok(process.env.SESSION_SECRET);
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
assert.ok(process.env.PORT);
|
assert.ok(process.env.PORT);
|
||||||
@@ -33,3 +34,4 @@ export const NODE_ENV = process.env.NODE_ENV;
|
|||||||
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
|
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
|
||||||
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
|
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
|
||||||
export const PORT = process.env.PORT || '3000';
|
export const PORT = process.env.PORT || '3000';
|
||||||
|
export const SESSION_SECRET = process.env.SESSION_SECRET;
|
||||||
|
|||||||
Generated
+35
@@ -113,15 +113,21 @@ importers:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@fastify/cookie': ^8.3.0
|
||||||
'@fastify/middie': '8.1'
|
'@fastify/middie': '8.1'
|
||||||
'@fastify/mongodb': 6.2.0
|
'@fastify/mongodb': 6.2.0
|
||||||
|
'@fastify/session': ^10.1.1
|
||||||
|
connect-mongo: ^4.6.0
|
||||||
fastify: 4.14.0
|
fastify: 4.14.0
|
||||||
fastify-auth0-verify: ^1.0.0
|
fastify-auth0-verify: ^1.0.0
|
||||||
fastify-plugin: ^4.3.0
|
fastify-plugin: ^4.3.0
|
||||||
nodemon: 2.0.21
|
nodemon: 2.0.21
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@fastify/cookie': 8.3.0
|
||||||
'@fastify/middie': 8.1.0
|
'@fastify/middie': 8.1.0
|
||||||
'@fastify/mongodb': 6.2.0
|
'@fastify/mongodb': 6.2.0
|
||||||
|
'@fastify/session': 10.1.1
|
||||||
|
connect-mongo: 4.6.0_lw7oj3533zfvrhxigrep3g2fam
|
||||||
fastify: 4.14.0
|
fastify: 4.14.0
|
||||||
fastify-auth0-verify: 1.0.0
|
fastify-auth0-verify: 1.0.0
|
||||||
fastify-plugin: 4.5.0
|
fastify-plugin: 4.5.0
|
||||||
@@ -4697,6 +4703,13 @@ packages:
|
|||||||
- aws-crt
|
- aws-crt
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@fastify/session/10.1.1:
|
||||||
|
resolution: {integrity: sha512-8pKDTL9MuqU1FCTca6XNd1E4quZ/ipik69AHXqkANia9Z4xPFS5OSKIwmCClIdaMYD32/tPu4G/6wGgK5Buj5g==}
|
||||||
|
dependencies:
|
||||||
|
fastify-plugin: 4.5.0
|
||||||
|
safe-stable-stringify: 2.4.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@fortawesome/fontawesome-common-types/6.3.0:
|
/@fortawesome/fontawesome-common-types/6.3.0:
|
||||||
resolution: {integrity: sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==}
|
resolution: {integrity: sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -12224,6 +12237,21 @@ packages:
|
|||||||
- snappy
|
- snappy
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/connect-mongo/4.6.0_lw7oj3533zfvrhxigrep3g2fam:
|
||||||
|
resolution: {integrity: sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
express-session: ^1.17.1
|
||||||
|
mongodb: ^4.1.0
|
||||||
|
dependencies:
|
||||||
|
debug: 4.3.4
|
||||||
|
express-session: 1.17.3
|
||||||
|
kruptein: 3.0.6
|
||||||
|
mongodb: 4.14.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/connect/3.7.0:
|
/connect/3.7.0:
|
||||||
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
|
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -19921,6 +19949,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
/kruptein/3.0.6:
|
||||||
|
resolution: {integrity: sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==}
|
||||||
|
engines: {node: '>8'}
|
||||||
|
dependencies:
|
||||||
|
asn1.js: 5.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/labeled-stream-splicer/2.0.2:
|
/labeled-stream-splicer/2.0.2:
|
||||||
resolution: {integrity: sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==}
|
resolution: {integrity: sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user