session management

This commit is contained in:
mrjvs
2023-10-28 18:34:32 +02:00
parent 94e1f9ebe1
commit 8f503b9c5a
13 changed files with 448 additions and 13 deletions

View File

@@ -1,5 +1,62 @@
import { Session } from '@/db/models/Session';
import { User } from '@/db/models/User';
import { Roles } from '@/services/access';
import { StatusError } from '@/services/error';
import { getSessionAndBump, verifySessionToken } from '@/services/session';
import { EntityManager } from '@mikro-orm/postgresql';
import { FastifyRequest } from 'fastify';
export function assertHasRole(_role: Roles) {
throw new Error('requires role');
export function makeAuthContext(manager: EntityManager, req: FastifyRequest) {
let userCache: User | null = null;
let sessionCache: Session | null = null;
const em = manager.fork();
return {
getSessionId(): string | null {
const header = req.headers.authorization;
if (!header) return null;
const [type, token] = header.split(' ', 2);
if (type.toLowerCase() !== 'Bearer')
throw new StatusError('Invalid auth', 400);
const payload = verifySessionToken(token);
if (!payload) throw new StatusError('Invalid auth', 400);
return payload.sid;
},
async getSession() {
if (sessionCache) return sessionCache;
const sid = this.getSessionId();
if (!sid) return null;
const session = await getSessionAndBump(em, sid);
if (session) return null;
sessionCache = session;
return session;
},
async getUser() {
if (userCache) return userCache;
const session = await this.getSession();
if (!session) return null;
const user = await em.findOne(User, { id: session.user });
if (!user) return null;
userCache = user;
return user;
},
async assert() {
const user = await this.getUser();
if (!user) throw new StatusError('Not logged in', 403);
return user;
},
get user() {
if (!userCache) throw new Error('call assert before getting user');
return userCache;
},
get session() {
if (!sessionCache) throw new Error('call assert before getting session');
return sessionCache;
},
async assertHasRole(role: Roles) {
const user = await this.assert();
const hasRole = user.roles.includes(role);
if (!hasRole) throw new StatusError('No permissions', 401);
},
};
}

9
src/services/error.ts Normal file
View File

@@ -0,0 +1,9 @@
export class StatusError extends Error {
errorStatusCode: number;
constructor(message: string, code: number) {
super(message);
this.errorStatusCode = code;
this.message = message;
}
}

View File

@@ -1,4 +1,5 @@
import { getORM } from '@/modules/mikro';
import { makeAuthContext } from '@/services/auth';
import { EntityManager } from '@mikro-orm/postgresql';
import {
ContextConfigDefault,
@@ -73,6 +74,7 @@ export type RequestContext<
Logger
>['query'];
em: EntityManager;
auth: ReturnType<typeof makeAuthContext>;
};
export function handle<
@@ -112,6 +114,7 @@ export function handle<
Logger
> {
const reqHandler: any = async (req: any, res: any) => {
const em = getORM().em.fork();
res.send(
await handler({
req,
@@ -119,7 +122,8 @@ export function handle<
body: req.body,
params: req.params,
query: req.query,
em: getORM().em.fork(),
em,
auth: makeAuthContext(em, req),
}),
);
};

69
src/services/session.ts Normal file
View File

@@ -0,0 +1,69 @@
import { conf } from '@/config';
import { Session } from '@/db/models/Session';
import { EntityManager } from '@mikro-orm/postgresql';
import { sign, verify } from 'jsonwebtoken';
// 21 days in ms
const SESSION_EXPIRY_MS = 21 * 24 * 60 * 60 * 1000;
export async function getSession(
em: EntityManager,
id: string,
): Promise<Session | null> {
const session = await em.findOne(Session, { id });
if (!session) return null;
if (session.expiresAt < new Date()) return null;
return session;
}
export async function getSessionAndBump(
em: EntityManager,
id: string,
): Promise<Session | null> {
const session = await getSession(em, id);
if (!session) return null;
em.assign(session, {
accessedAt: new Date(),
expiresAt: new Date(Date.now() + SESSION_EXPIRY_MS),
});
await em.flush();
return session;
}
export function makeSession(
user: string,
device: string,
userAgent?: string,
): Session {
if (!userAgent) throw new Error('No useragent provided');
const session = new Session();
session.accessedAt = new Date();
session.createdAt = new Date();
session.expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
session.userAgent = userAgent;
session.device = device;
session.user = user;
return session;
}
export function makeSessionToken(session: Session): string {
return sign({ sid: session.id }, conf.crypto.sessionSecret, {
algorithm: 'ES512',
});
}
export function verifySessionToken(token: string): { sid: string } | null {
try {
const payload = verify(token, conf.crypto.sessionSecret, {
algorithms: ['ES512'],
});
if (typeof payload === 'string') return null;
return payload as { sid: string };
} catch {
return null;
}
}