135 lines
3.2 KiB
TypeScript
135 lines
3.2 KiB
TypeScript
import uid from "uid-safe";
|
|
import dayjs from "dayjs";
|
|
import { db } from "~/database";
|
|
import type { User } from "~/user/user";
|
|
|
|
interface Session {
|
|
id: string;
|
|
signedId: string;
|
|
userId: string;
|
|
durationMs: number;
|
|
expiresAt: number;
|
|
}
|
|
|
|
const SESSION_ID_BYTE_LENGTH = 24;
|
|
const SESSION_ID_COOKIE_NAME = "session-id";
|
|
const SESSION_DURATION_MS = 30 * 60 * 1000;
|
|
|
|
const findSessionQuery = db.query(
|
|
"SELECT user_id, expires_at_unix FROM sessions WHERE session_id = $sessionId",
|
|
);
|
|
|
|
const deleteSessionQuery = db.query(
|
|
"DELETE FROM sessions WHERE session_id = $sessionId",
|
|
);
|
|
|
|
const forgetAllSessionsQuery = db.query(
|
|
"DELETE FROM sessions WHERE user_id = $userId",
|
|
);
|
|
|
|
const saveSessionQuery = db.query(
|
|
"INSERT INTO sessions(session_id, user_id, expires_at_unix_ms) VALUES ($sessionId, $userId, $expiresAt)",
|
|
);
|
|
|
|
const extendSessionQuery = db.query(
|
|
"UPDATE sessions SET expires_at_unix_ms = $newExpiryDate WHERE session_id = $session_id",
|
|
);
|
|
|
|
async function newSessionId(): Promise<string> {
|
|
return await uid(SESSION_ID_BYTE_LENGTH);
|
|
}
|
|
|
|
function signSessionId(sessionId: string): string {
|
|
const hasher = new Bun.CryptoHasher("sha256", Bun.env.SESSION_SECRET);
|
|
hasher.update(sessionId);
|
|
return `${sessionId}.${hasher.digest("base64url")}`;
|
|
}
|
|
|
|
async function createSessionForUser(user: User, cookies: Bun.CookieMap) {
|
|
const sessionId = await newSessionId();
|
|
const signedSessionId = signSessionId(sessionId);
|
|
|
|
const expiryDate = dayjs().add(30, "minutes").valueOf();
|
|
|
|
saveSessionQuery.run({
|
|
sessionId,
|
|
userId: user.id,
|
|
expiresAt: expiryDate,
|
|
});
|
|
|
|
cookies.set(SESSION_ID_COOKIE_NAME, signedSessionId, {
|
|
maxAge: SESSION_DURATION_MS,
|
|
httpOnly: true,
|
|
});
|
|
}
|
|
|
|
async function saveSession(session: Session, cookies: Bun.CookieMap) {
|
|
cookies.set(SESSION_ID_COOKIE_NAME, session.signedId, {
|
|
maxAge: SESSION_DURATION_MS,
|
|
httpOnly: true,
|
|
});
|
|
}
|
|
|
|
function verifySession(cookie: Bun.CookieMap): Session | null {
|
|
const signedSessionId = cookie.get(SESSION_ID_COOKIE_NAME);
|
|
if (!signedSessionId) {
|
|
return null;
|
|
}
|
|
|
|
const value = signedSessionId.slice(0, signedSessionId.lastIndexOf("."));
|
|
const expected = signSessionId(value);
|
|
|
|
const a = Buffer.from(signedSessionId);
|
|
const b = Buffer.from(expected);
|
|
|
|
const isEqual = a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
if (!isEqual) {
|
|
return null;
|
|
}
|
|
|
|
const row = findSessionQuery.get({ sessionId: value });
|
|
if (!row) {
|
|
return null;
|
|
}
|
|
const foundSession = row as { user_id: string; expires_at_unix_ms: number };
|
|
|
|
const now = dayjs().valueOf();
|
|
if (now > foundSession.expires_at_unix_ms) {
|
|
deleteSessionQuery.run({ sessionId: value });
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: value,
|
|
signedId: signedSessionId,
|
|
userId: foundSession.user_id,
|
|
expiresAt: foundSession.expires_at_unix_ms,
|
|
durationMs: SESSION_DURATION_MS,
|
|
};
|
|
}
|
|
|
|
function extendSession(session: Session): Session {
|
|
const newExpiryDate = dayjs().add(30, "minutes").valueOf();
|
|
extendSessionQuery.run({
|
|
sessionId: session.id,
|
|
newExpiryDate,
|
|
});
|
|
return {
|
|
...session,
|
|
expiresAt: newExpiryDate,
|
|
};
|
|
}
|
|
|
|
function forgetAllSessions(user: User) {
|
|
forgetAllSessionsQuery.run({ userId: user.id });
|
|
}
|
|
|
|
export {
|
|
newSessionId,
|
|
createSessionForUser,
|
|
verifySession,
|
|
saveSession,
|
|
extendSession,
|
|
forgetAllSessions,
|
|
};
|