2025-06-30 18:58:24 +02:00
|
|
|
/*
|
|
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
*/
|
2025-03-07 12:41:57 +01:00
|
|
|
import type { H3Event } from "h3";
|
2025-07-07 22:42:49 +02:00
|
|
|
import {
|
|
|
|
nextSessionId,
|
|
|
|
readSessions,
|
|
|
|
readSubscriptions,
|
|
|
|
readUsers,
|
|
|
|
type ServerSession,
|
|
|
|
type ServerUser,
|
|
|
|
writeSessions,
|
|
|
|
writeSubscriptions
|
|
|
|
} from "~/server/database";
|
2025-07-08 15:43:14 +02:00
|
|
|
import { broadcastEvent } from "../streams";
|
|
|
|
import type { ApiSession } from "~/shared/types/api";
|
2025-03-11 16:30:51 +01:00
|
|
|
|
2025-03-07 14:11:07 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-09 16:51:05 +02:00
|
|
|
async function clearServerSessionInternal(event: H3Event, sessions: ServerSession[]) {
|
2025-03-07 12:41:57 +01:00
|
|
|
const existingSessionCookie = await getSignedCookie(event, "session");
|
|
|
|
if (existingSessionCookie) {
|
|
|
|
const sessionId = parseInt(existingSessionCookie, 10);
|
2025-07-08 15:43:14 +02:00
|
|
|
const session = sessions.find(session => session.id === sessionId);
|
|
|
|
if (session) {
|
2025-07-09 14:54:54 +02:00
|
|
|
session.expiresAtMs = Date.now();
|
2025-07-08 15:43:14 +02:00
|
|
|
broadcastEvent({
|
|
|
|
type: "session-expired",
|
|
|
|
sessionId,
|
|
|
|
});
|
2025-03-07 14:11:07 +01:00
|
|
|
await removeSessionSubscription(sessionId);
|
2025-03-07 12:41:57 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2025-06-09 16:51:05 +02:00
|
|
|
export async function clearServerSession(event: H3Event) {
|
2025-03-07 12:41:57 +01:00
|
|
|
const sessions = await readSessions();
|
2025-06-09 16:51:05 +02:00
|
|
|
if (await clearServerSessionInternal(event, sessions)) {
|
2025-03-07 12:41:57 +01:00
|
|
|
await writeSessions(sessions);
|
|
|
|
}
|
2025-03-08 00:36:10 +01:00
|
|
|
deleteCookie(event, "session");
|
2025-03-07 12:41:57 +01:00
|
|
|
}
|
|
|
|
|
2025-07-09 15:21:39 +02:00
|
|
|
export async function setServerSession(
|
|
|
|
event: H3Event,
|
|
|
|
account: ServerUser | undefined,
|
|
|
|
authenticationProvider?: "telegram",
|
|
|
|
authenticationSlug?: string,
|
|
|
|
authenticationName?: string,
|
|
|
|
) {
|
2025-03-07 12:41:57 +01:00
|
|
|
const sessions = await readSessions();
|
2025-07-07 22:42:49 +02:00
|
|
|
const runtimeConfig = useRuntimeConfig(event);
|
2025-06-09 16:51:05 +02:00
|
|
|
await clearServerSessionInternal(event, sessions);
|
2025-03-07 12:41:57 +01:00
|
|
|
|
2025-07-07 22:42:49 +02:00
|
|
|
const now = Date.now();
|
2025-06-09 16:51:05 +02:00
|
|
|
const newSession: ServerSession = {
|
2025-07-09 14:54:54 +02:00
|
|
|
access: account?.type ?? "anonymous",
|
|
|
|
accountId: account?.id,
|
2025-07-09 15:21:39 +02:00
|
|
|
authenticationProvider,
|
|
|
|
authenticationSlug,
|
|
|
|
authenticationName,
|
2025-07-09 14:54:54 +02:00
|
|
|
rotatesAtMs: now + runtimeConfig.sessionRotatesTimeout * 1000,
|
2025-07-07 22:42:49 +02:00
|
|
|
discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000,
|
2025-03-07 12:41:57 +01:00
|
|
|
id: await nextSessionId(),
|
|
|
|
};
|
|
|
|
|
|
|
|
sessions.push(newSession);
|
|
|
|
await writeSessions(sessions);
|
2025-07-07 22:42:49 +02:00
|
|
|
await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout)
|
2025-07-09 15:21:39 +02:00
|
|
|
return newSession;
|
2025-03-11 16:30:51 +01:00
|
|
|
}
|
|
|
|
|
2025-07-07 22:42:49 +02:00
|
|
|
async function rotateSession(event: H3Event, sessions: ServerSession[], session: ServerSession) {
|
|
|
|
const runtimeConfig = useRuntimeConfig(event);
|
|
|
|
const users = await readUsers();
|
2025-07-08 16:23:31 +02:00
|
|
|
const account = users.find(user => !user.deleted && user.id === session.accountId);
|
2025-07-07 22:42:49 +02:00
|
|
|
const now = Date.now();
|
|
|
|
const newSession: ServerSession = {
|
|
|
|
accountId: account?.id,
|
|
|
|
access: account?.type ?? "anonymous",
|
2025-07-09 15:21:39 +02:00
|
|
|
// Authentication provider is removed to avoid possibility of an infinite delay before using it.
|
2025-07-09 14:54:54 +02:00
|
|
|
rotatesAtMs: now + runtimeConfig.sessionRotatesTimeout * 1000,
|
2025-07-07 22:42:49 +02:00
|
|
|
discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000,
|
|
|
|
id: await nextSessionId(),
|
|
|
|
};
|
|
|
|
session.successor = newSession.id;
|
2025-07-09 14:54:54 +02:00
|
|
|
session.expiresAtMs = Date.now() + 10 * 1000;
|
2025-07-07 22:42:49 +02:00
|
|
|
sessions.push(newSession);
|
|
|
|
await writeSessions(sessions);
|
|
|
|
await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout)
|
|
|
|
return newSession;
|
2025-03-07 12:41:57 +01:00
|
|
|
}
|
|
|
|
|
2025-07-09 14:54:54 +02:00
|
|
|
export async function getServerSession(event: H3Event, ignoreTaken: boolean) {
|
2025-03-07 12:41:57 +01:00
|
|
|
const sessionCookie = await getSignedCookie(event, "session");
|
|
|
|
if (sessionCookie) {
|
|
|
|
const sessionId = parseInt(sessionCookie, 10);
|
|
|
|
const sessions = await readSessions();
|
2025-07-07 22:42:49 +02:00
|
|
|
const session = sessions.find(session => session.id === sessionId);
|
|
|
|
if (session) {
|
2025-07-09 14:54:54 +02:00
|
|
|
const nowMs = Date.now();
|
|
|
|
if (nowMs >= session.discardAtMs) {
|
2025-07-08 15:43:14 +02:00
|
|
|
return undefined;
|
|
|
|
}
|
2025-07-09 14:54:54 +02:00
|
|
|
if (session.expiresAtMs !== undefined && nowMs >= session.expiresAtMs) {
|
|
|
|
if (!ignoreTaken && session.successor !== undefined) {
|
|
|
|
throw createError({
|
|
|
|
statusCode: 403,
|
|
|
|
statusMessage: "Forbidden",
|
|
|
|
message: "Session has been taken by another agent.",
|
|
|
|
data: { code: "SESSION_TAKEN" },
|
|
|
|
});
|
|
|
|
}
|
2025-07-07 22:42:49 +02:00
|
|
|
return undefined;
|
|
|
|
}
|
2025-07-09 14:54:54 +02:00
|
|
|
if (nowMs >= session.rotatesAtMs && session.successor === undefined) {
|
2025-07-07 22:42:49 +02:00
|
|
|
return await rotateSession(event, sessions, session);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return session;
|
2025-03-07 12:41:57 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-07 22:42:49 +02:00
|
|
|
export async function requireServerSession(event: H3Event, message: string) {
|
|
|
|
const session = await getServerSession(event, false);
|
2025-03-07 12:41:57 +01:00
|
|
|
if (!session)
|
|
|
|
throw createError({
|
2025-07-07 22:42:49 +02:00
|
|
|
statusCode: 401,
|
|
|
|
statusMessage: "Unauthorized",
|
|
|
|
message,
|
2025-03-07 12:41:57 +01:00
|
|
|
});
|
|
|
|
return session;
|
|
|
|
}
|
2025-06-28 00:55:26 +02:00
|
|
|
|
2025-07-07 22:42:49 +02:00
|
|
|
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);
|
2025-07-08 16:23:31 +02:00
|
|
|
if (session.accountId === undefined || !account || account.deleted)
|
2025-07-07 22:42:49 +02:00
|
|
|
throw createError({
|
|
|
|
statusCode: 401,
|
|
|
|
statusMessage: "Uauthorized",
|
|
|
|
message,
|
|
|
|
});
|
|
|
|
return { ...session, accountId: session.accountId };
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-06-28 00:55:26 +02:00
|
|
|
export async function requireServerSessionWithAdmin(event: H3Event) {
|
2025-07-07 22:42:49 +02:00
|
|
|
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") {
|
2025-06-28 00:55:26 +02:00
|
|
|
throw createError({
|
|
|
|
statusCode: 403,
|
|
|
|
statusMessage: "Forbidden",
|
2025-07-07 22:42:49 +02:00
|
|
|
message,
|
2025-06-28 00:55:26 +02:00
|
|
|
});
|
|
|
|
}
|
2025-07-07 22:42:49 +02:00
|
|
|
return { ...session, accountId: session.accountId };
|
2025-06-28 00:55:26 +02:00
|
|
|
}
|
2025-07-08 15:43:14 +02:00
|
|
|
|
|
|
|
export async function serverSessionToApi(event: H3Event, session: ServerSession): Promise<ApiSession> {
|
|
|
|
const users = await readUsers();
|
2025-07-08 16:23:31 +02:00
|
|
|
const account = users.find(user => !user.deleted && user.id === session.accountId);
|
2025-07-08 15:43:14 +02:00
|
|
|
const subscriptions = await readSubscriptions();
|
|
|
|
const push = Boolean(
|
|
|
|
subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id)
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: session.id,
|
|
|
|
account,
|
2025-07-09 15:21:39 +02:00
|
|
|
authenticationProvider: session.authenticationProvider,
|
|
|
|
authenticationName: session.authenticationName,
|
2025-07-08 15:43:14 +02:00
|
|
|
push,
|
|
|
|
};
|
|
|
|
}
|