mirror of
https://github.com/movie-web/backend.git
synced 2025-09-13 18:13:26 +00:00
session management
This commit is contained in:
@@ -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
9
src/services/error.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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
69
src/services/session.ts
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user