/* SPDX-FileCopyrightText: © 2025 Hornwitser SPDX-License-Identifier: AGPL-3.0-or-later */ import type { H3Event } from "h3"; import { nextSessionId, readSessions, readSubscriptions, readUsers, type ServerSession, type ServerUser, writeSessions, writeSubscriptions } from "~/server/database"; import { broadcastEvent } from "../streams"; import type { ApiSession } from "~/shared/types/api"; async function removeSessionSubscription(sessionId: number) { const subscriptions = await readSubscriptions(); const index = subscriptions.findIndex(subscription => subscription.sessionId === sessionId); if (index !== -1) { subscriptions.splice(index, 1); await writeSubscriptions(subscriptions); } } async function clearServerSessionInternal(event: H3Event, sessions: ServerSession[]) { const existingSessionCookie = await getSignedCookie(event, "session"); if (existingSessionCookie) { const sessionId = parseInt(existingSessionCookie, 10); const session = sessions.find(session => session.id === sessionId); if (session) { session.finished = true; broadcastEvent({ type: "session-expired", sessionId, }); await removeSessionSubscription(sessionId); return true; } } return false; } export async function clearServerSession(event: H3Event) { const sessions = await readSessions(); if (await clearServerSessionInternal(event, sessions)) { await writeSessions(sessions); } deleteCookie(event, "session"); } export async function setServerSession(event: H3Event, account: ServerUser) { const sessions = await readSessions(); const runtimeConfig = useRuntimeConfig(event); await clearServerSessionInternal(event, sessions); const now = Date.now(); const newSession: ServerSession = { accountId: account.id, access: account.type, expiresAtMs: now + runtimeConfig.sessionExpiresTimeout * 1000, discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000, finished: false, id: await nextSessionId(), }; sessions.push(newSession); await writeSessions(sessions); await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout) } async function rotateSession(event: H3Event, sessions: ServerSession[], session: ServerSession) { const runtimeConfig = useRuntimeConfig(event); const users = await readUsers(); const account = users.find(user => user.id === session.accountId); const now = Date.now(); const newSession: ServerSession = { accountId: account?.id, access: account?.type ?? "anonymous", expiresAtMs: now + runtimeConfig.sessionExpiresTimeout * 1000, discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000, finished: false, id: await nextSessionId(), }; session.successor = newSession.id; sessions.push(newSession); await writeSessions(sessions); await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout) return newSession; } export async function getServerSession(event: H3Event, ignoreExpired: boolean) { const sessionCookie = await getSignedCookie(event, "session"); if (sessionCookie) { const sessionId = parseInt(sessionCookie, 10); const sessions = await readSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { if (session.finished) { return undefined; } if (!ignoreExpired && session.successor !== undefined) { throw createError({ statusCode: 403, statusMessage: "Forbidden", message: "Session has been taken by another agent.", data: { code: "SESSION_TAKEN" }, }); } const now = Date.now(); if (now >= session?.discardAtMs) { return undefined; } if (!ignoreExpired && now >= session?.expiresAtMs) { return await rotateSession(event, sessions, session); } } return session; } } export async function requireServerSession(event: H3Event, message: string) { const session = await getServerSession(event, false); if (!session) throw createError({ statusCode: 401, statusMessage: "Unauthorized", message, }); return session; } export async function requireServerSessionWithUser(event: H3Event) { const message = "User session required"; const session = await requireServerSession(event, message); const users = await readUsers(); const account = users.find(user => user.id === session.accountId); if (session.accountId === undefined || !account) throw createError({ statusCode: 401, statusMessage: "Uauthorized", message, }); return { ...session, accountId: session.accountId }; } export async function requireServerSessionWithAdmin(event: H3Event) { const message = "Admin session required"; const session = await requireServerSession(event, message); const users = await readUsers(); const account = users.find(user => user.id === session.accountId); if (session.access !== "admin" || account?.type !== "admin") { throw createError({ statusCode: 403, statusMessage: "Forbidden", message, }); } return { ...session, accountId: session.accountId }; } export async function serverSessionToApi(event: H3Event, session: ServerSession): Promise { const users = await readUsers(); const account = users.find(user => user.id === session.accountId); const subscriptions = await readSubscriptions(); const push = Boolean( subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id) ); return { id: session.id, account, push, }; }