import type { H3Event } from "h3"; import type { Session, SessionProvider } from "./types"; import { randomUUID } from "node:crypto"; import { parse as parseCookies } from "cookie-es"; import type { MinimumRequestObject } from "~/server/h3"; import type { DurationLike } from "luxon"; import { DateTime } from "luxon"; import createDBSessionHandler from "./db"; /* This implementation may need work. It exposes an API that should stay static, but there are plenty of opportunities for optimisation/organisation under the hood */ const dropTokenCookieName = "drop-token"; const normalSessionLength: DurationLike = { days: 31, }; const extendedSessionLength: DurationLike = { year: 1, }; export class SessionHandler { private sessionProvider: SessionProvider; constructor() { // Create a new provider // this.sessionProvider = createCacheSessionProvider(); this.sessionProvider = createDBSessionHandler(); // this.sessionProvider = createMemorySessionProvider(); } async signin(h3: H3Event, userId: string, rememberMe: boolean = false) { const expiresAt = this.createExipreAt(rememberMe); const token = this.createSessionCookie(h3, expiresAt); return await this.sessionProvider.setSession(token, { userId, expiresAt, data: {}, }); } /** * Get a session associated with a request * @returns session */ async getSession(request: MinimumRequestObject) { const token = this.getSessionToken(request); if (!token) return undefined; // TODO: should validate if session is expired or not here, not in application code const data = await this.sessionProvider.getSession(token); return data; } /** * Signout session associated with request and deauthenticates it * @param request * @returns */ async signout(h3: H3Event) { const token = this.getSessionToken(h3); if (!token) return false; const res = await this.sessionProvider.removeSession(token); if (!res) return false; deleteCookie(h3, dropTokenCookieName); return true; } async cleanupSessions() { await this.sessionProvider.cleanupSessions(); } /** * Update session info * @param token session token * @param data new session data * @returns success or not */ private async updateSession(token: string, data: Session) { return await this.sessionProvider.updateSession(token, data); } // ---------------------- Private API Below ------------------------ /** * Get session token on a request * @param request * @returns session token */ private getSessionToken( request: MinimumRequestObject | undefined, ): string | undefined { if (!request) throw new Error("Native web request not available"); const cookieHeader = request.headers.get("Cookie"); if (!cookieHeader) return undefined; const cookies = parseCookies(cookieHeader); const cookie = cookies[dropTokenCookieName]; return cookie; } private createExipreAt(rememberMe: boolean) { return DateTime.now() .plus(rememberMe ? extendedSessionLength : normalSessionLength) .toJSDate(); } /** * Creates cookie that represents user session * @param h3 * @param extend * @returns */ private createSessionCookie(h3: H3Event, expiresAt: Date) { const token = randomUUID(); // TODO: we should probably switch to jwts to minimize possibility of someone // trying to guess a session id (jwts let us sign + encrypt stuff in a std way) setCookie(h3, dropTokenCookieName, token, { expires: expiresAt }); return token; } } export const sessionHandler = new SessionHandler(); export default sessionHandler;