SPB Git
3.6 KB · 53 lines typescript
1import "server-only";2import { cookies, headers } from "next/headers";3import { createHash, randomBytes } from "node:crypto";4import { and, eq, gt, isNull, lt } from "drizzle-orm";5import { db, schema } from "@/db";6import { cache } from "react";78export const COOKIE = process.env.NODE_ENV === "production" ? "__Host-note_session" : "note_session";9const IDLE_DAYS = 30, ABS_DAYS = 90;10const sha = (s: string) => createHash("sha256").update(s).digest("hex");1112export async function createSession(userId: string, opts: { mfaPassed?: boolean } = {}) {13  const token = randomBytes(32).toString("base64url");14  const h = await headers();15  const now = new Date();16  await db.insert(schema.sessions).values({17    userId, tokenHash: sha(token), userAgent: h.get("user-agent")?.slice(0, 300) ?? null, ip: clientIp(h),18    expiresAt: new Date(now.getTime() + IDLE_DAYS * 864e5), absoluteExpiresAt: new Date(now.getTime() + ABS_DAYS * 864e5), mfaPassed: opts.mfaPassed ?? false,19  });20  return token;21}22export function clientIp(h: Headers) { return (h.get("x-forwarded-for")?.split(",")[0] ?? h.get("x-real-ip") ?? "0.0.0.0").trim(); }2324export async function setSessionCookie(token: string) {25  const c = await cookies();26  c.set(COOKIE, token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: ABS_DAYS * 86400 });27}28export async function clearSessionCookie() { (await cookies()).set(COOKIE, "", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 0 }); }2930export type SessionUser = { id: string; username: string; displayName: string; email: string | null; totpEnabled: boolean; preferences: Record<string, unknown>; sessionId: string; mfaPassed: boolean };3132/** Resolve the current session (cached per request). Returns null when unauthenticated. */33export const getSession = cache(async (): Promise<SessionUser | null> => {34  const token = (await cookies()).get(COOKIE)?.value;35  if (!token) return null;36  const now = new Date();37  const rows = await db.select({ s: schema.sessions, u: schema.users }).from(schema.sessions).innerJoin(schema.users, eq(schema.users.id, schema.sessions.userId))38    .where(and(eq(schema.sessions.tokenHash, sha(token)), isNull(schema.sessions.revokedAt), gt(schema.sessions.expiresAt, now), gt(schema.sessions.absoluteExpiresAt, now))).limit(1);39  if (!rows.length) return null;40  const { s, u } = rows[0];41  if (now.getTime() - s.lastSeenAt.getTime() > 10 * 60_000) {42    db.update(schema.sessions).set({ lastSeenAt: now, expiresAt: new Date(now.getTime() + IDLE_DAYS * 864e5) }).where(eq(schema.sessions.id, s.id)).catch(() => {});43  }44  return { id: u.id, username: u.username, displayName: u.displayName, email: u.email, totpEnabled: u.totpEnabled, preferences: u.preferences, sessionId: s.id, mfaPassed: s.mfaPassed };45});4647export async function revokeSession(sessionId: string) { await db.update(schema.sessions).set({ revokedAt: new Date() }).where(eq(schema.sessions.id, sessionId)); }48export async function revokeAllSessions(userId: string, exceptId?: string) {49  await db.update(schema.sessions).set({ revokedAt: new Date() }).where(and(eq(schema.sessions.userId, userId), isNull(schema.sessions.revokedAt), exceptId ? lt(schema.sessions.id, exceptId) : undefined));50  if (exceptId) await db.update(schema.sessions).set({ revokedAt: new Date() }).where(and(eq(schema.sessions.userId, userId), isNull(schema.sessions.revokedAt), gt(schema.sessions.id, exceptId)));51}52export async function markMfaPassed(sessionId: string) { await db.update(schema.sessions).set({ mfaPassed: true }).where(eq(schema.sessions.id, sessionId)); }53
Files · main
Menu

Theme