forked from atums.world/backend
re-order alot, move to bun redis, generalized
This commit is contained in:
parent
a646607597
commit
8a9499be85
51 changed files with 559 additions and 916 deletions
145
src/lib/jwt.ts
Normal file
145
src/lib/jwt.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
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,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue