backend/src/lib/jwt.ts
creations 9d8b3eb969
All checks were successful
Code quality checks / biome (push) Successful in 8s
seperate all config files, move so i can just call @config
2025-05-03 08:00:36 -04:00

145 lines
3.9 KiB
TypeScript

import { environment, jwt } from "@config";
import { redis } from "bun";
import { createDecoder, createSigner, createVerifier } from "fast-jwt";
const signer = createSigner({ key: jwt.secret, expiresIn: jwt.expiration });
const verifier = createVerifier({ key: jwt.secret });
const decoder = createDecoder();
export async function createSession(
payload: UserSession,
userAgent: string,
): Promise<string> {
const token = signer(payload);
const sessionKey = `session:${payload.id}:${token}`;
await redis.set(sessionKey, JSON.stringify({ ...payload, userAgent }));
await redis.expire(sessionKey, getExpirationInSeconds());
return generateCookie(token);
}
export async function getSession(
request: Request,
): Promise<UserSession | null> {
const token = extractToken(request);
if (!token) return null;
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) return null;
const raw = await redis.get(keys[0]);
return raw ? JSON.parse(raw) : null;
}
export async function updateSession(
request: Request,
payload: UserSession,
userAgent: string,
): Promise<string> {
const token = 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");
await redis.set(keys[0], JSON.stringify({ ...payload, userAgent }));
await redis.expire(keys[0], getExpirationInSeconds());
return generateCookie(token);
}
export async function verifySession(token: string): Promise<UserSession> {
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) throw new Error("Session not found or expired");
return verifier(token);
}
export async function decodeSession(token: string): Promise<UserSession> {
return decoder(token);
}
export async function invalidateSession(request: Request): Promise<void> {
const token = extractToken(request);
if (!token) return;
const keys = await redis.keys(`session:*:${token}`);
if (!keys.length) return;
await redis.del(keys[0]);
}
export async function invalidateSessionById(
sessionId: string,
): Promise<boolean> {
const keys = await redis.keys(`session:*:${sessionId}`);
if (!keys.length) return false;
await redis.del(keys[0]);
return true;
}
export async function 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;
}
// helpers
function extractToken(request: Request): string | null {
return request.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1] || null;
}
function generateCookie(
token: string,
maxAge = getExpirationInSeconds(),
options?: {
secure?: boolean;
httpOnly?: boolean;
sameSite?: "Strict" | "Lax" | "None";
path?: string;
domain?: string;
},
): string {
const {
secure = !environment.development,
httpOnly = true,
sameSite = environment.development ? "Lax" : "None",
path = "/",
domain,
} = options || {};
let cookie = `session=${encodeURIComponent(token)}; Path=${path}; Max-Age=${maxAge}`;
if (httpOnly) cookie += "; HttpOnly";
if (secure) cookie += "; Secure";
if (sameSite) cookie += `; SameSite=${sameSite}`;
if (domain) cookie += `; Domain=${domain}`;
return cookie;
}
function getExpirationInSeconds(): number {
const match = jwt.expiration.match(/^(\d+)([smhd])$/);
if (!match) throw new Error("Invalid expiresIn format in jwt config");
const [, value, unit] = match;
const num = Number(value);
switch (unit) {
case "s":
return num;
case "m":
return num * 60;
case "h":
return num * 3600;
case "d":
return num * 86400;
default:
throw new Error("Invalid time unit in expiresIn");
}
}
export const sessionManager = {
createSession,
getSession,
updateSession,
verifySession,
decodeSession,
invalidateSession,
invalidateSessionById,
invalidateAllSessionsForUser,
};