145 lines
3.9 KiB
TypeScript
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,
|
|
};
|