import { redis } from "bun"; import { jwt } from "#environment/jwt"; import { cookieService, jwtService } from "#lib/auth"; import type { CookieOptions, SessionData, UserSession } from "#types/config"; import type { ExtendedRequest } from "#types/server"; class SessionManager { async createSession( payload: UserSession, userAgent: string, cookieOptions?: CookieOptions, ): Promise { const token = jwtService.sign(payload); const sessionKey = this.getSessionKey(payload.id, token); const sessionData: SessionData = { ...payload, userAgent }; await redis.set(sessionKey, JSON.stringify(sessionData)); await redis.expire(sessionKey, jwt.expiration as number); return cookieService.generateCookie( token, jwt.expiration as number, cookieOptions, ); } async getSession( request: Request | ExtendedRequest, ): Promise { const token = cookieService.extractToken(request); if (!token) return null; return this.getSessionByToken(token); } async getSessionByToken(token: string): Promise { const keys = await redis.keys(`session:*:${token}`); if (!keys.length) return null; const sessionKey = keys[0]; if (!sessionKey) return null; const raw = await redis.get(sessionKey); if (!raw) return null; try { const sessionData: SessionData = JSON.parse(raw); const { userAgent, ...userSession } = sessionData; return userSession; } catch { return null; } } async updateSession( request: Request | ExtendedRequest, payload: UserSession, userAgent: string, cookieOptions?: CookieOptions, ): Promise { const token = cookieService.extractToken(request); if (!token) throw new Error("Session token not found"); const keys = await redis.keys(`session:*:${token}`); if (!keys.length) throw new Error("Session not found or expired"); const sessionKey = keys[0]; if (!sessionKey) throw new Error("Session not found or expired"); const sessionData: SessionData = { ...payload, userAgent }; await redis.set(sessionKey, JSON.stringify(sessionData)); await redis.expire(sessionKey, jwt.expiration as number); return cookieService.generateCookie( token, jwt.expiration as number, cookieOptions, ); } async refreshSession( request: Request | ExtendedRequest, cookieOptions?: CookieOptions, ): Promise { const token = cookieService.extractToken(request); if (!token) return null; const keys = await redis.keys(`session:*:${token}`); if (!keys.length) return null; const sessionKey = keys[0]; if (!sessionKey) return null; await redis.expire(sessionKey, jwt.expiration as number); return cookieService.generateCookie( token, jwt.expiration as number, cookieOptions, ); } async verifySession(token: string): Promise { const keys = await redis.keys(`session:*:${token}`); if (!keys.length) throw new Error("Session not found or expired"); return jwtService.verify(token); } async decodeSession(token: string): Promise { return jwtService.decode(token); } async invalidateSession(request: Request | ExtendedRequest): Promise { const token = cookieService.extractToken(request); if (!token) return; await this.invalidateSessionByToken(token); } async invalidateSessionByToken(token: string): Promise { const keys = await redis.keys(`session:*:${token}`); if (!keys.length) return; const sessionKey = keys[0]; if (!sessionKey) return; await redis.del(sessionKey); } async invalidateSessionById(sessionId: string): Promise { const keys = await redis.keys(`session:*:${sessionId}`); if (!keys.length) return false; const sessionKey = keys[0]; if (!sessionKey) return false; await redis.del(sessionKey); return true; } async invalidateAllSessionsForUser(userId: string): Promise { const keys = await redis.keys(`session:${userId}:*`); if (keys.length === 0) return 0; for (const key of keys) { await redis.del(key); } return keys.length; } async getActiveSessionsForUser(userId: string): Promise { const keys = await redis.keys(`session:${userId}:*`); return keys.flatMap((key) => { const token = key.split(":")[2]; return token ? [token] : []; }); } private getSessionKey(userId: string, token: string): string { return `session:${userId}:${token}`; } } const sessionManager = new SessionManager(); export { SessionManager, sessionManager };