All checks were successful
Code quality checks / biome (push) Successful in 11s
167 lines
4.4 KiB
TypeScript
167 lines
4.4 KiB
TypeScript
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<string> {
|
|
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<UserSession | null> {
|
|
const token = cookieService.extractToken(request);
|
|
if (!token) return null;
|
|
|
|
return this.getSessionByToken(token);
|
|
}
|
|
|
|
async getSessionByToken(token: string): Promise<UserSession | null> {
|
|
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<string> {
|
|
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<string | null> {
|
|
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<UserSession> {
|
|
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<UserSession> {
|
|
return jwtService.decode(token);
|
|
}
|
|
|
|
async invalidateSession(request: Request | ExtendedRequest): Promise<void> {
|
|
const token = cookieService.extractToken(request);
|
|
if (!token) return;
|
|
|
|
await this.invalidateSessionByToken(token);
|
|
}
|
|
|
|
async invalidateSessionByToken(token: string): Promise<void> {
|
|
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<boolean> {
|
|
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<number> {
|
|
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<string[]> {
|
|
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 };
|