owltide/server/database.ts
Hornwitser 0d0e38e4b6 Refactor demo login as an authentication method
Use the authentication method system for the demo login and the
generated accounts.  This makes it possible to toggle it off on
production systems as these shouldn't have it enabled at all.
2025-07-09 18:01:26 +02:00

170 lines
4.8 KiB
TypeScript

/*
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { readFile, unlink, writeFile } from "node:fs/promises";
import type { ApiAuthenticationProvider, ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api";
import type { Id } from "~/shared/types/common";
export interface ServerSession {
id: Id,
access: ApiUserType,
accountId?: number,
authenticationProvider?: ApiAuthenticationProvider,
authenticationSlug?: string,
authenticationName?: string,
rotatesAtMs: number,
expiresAtMs?: number,
discardAtMs: number,
successor?: Id,
};
export interface ServerUser {
id: Id,
updatedAt: string,
deleted?: boolean,
type: ApiUserType,
name?: string,
interestedEventIds?: number[],
interestedEventSlotIds?: number[],
timezone?: string,
locale?: string,
}
export interface ServerAuthenticationMethod {
id: Id,
provider: ApiAuthenticationProvider,
slug: string,
name: string,
userId: Id,
}
// For this demo I'm just storing the runtime data in JSON files. When making
// this into proper application this should be replaced with an actual database.
const schedulePath = "data/schedule.json";
const subscriptionsPath = "data/subscriptions.json";
const usersPath = "data/users.json";
const nextUserIdPath = "data/next-user-id.json";
const sessionsPath = "data/sessions.json";
const nextSessionIdPath = "data/next-session-id.json";
const authMethodPath = "data/auth-method.json";
const nextAuthenticationMethodIdPath = "data/auth-method-id.json"
async function remove(path: string) {
try {
await unlink(path);
} catch (err: any) {
if (err.code !== "ENOENT") {
throw err;
}
}
}
export async function deleteDatabase() {
await remove(schedulePath);
await remove(subscriptionsPath);
await remove(usersPath);
await remove(sessionsPath);
}
async function readJson<T>(filePath: string, fallback: T) {
let data: T extends () => infer R ? R : T;
try {
data = JSON.parse(await readFile(filePath, "utf-8"));
} catch (err: any) {
if (err.code !== "ENOENT")
throw err;
data = typeof fallback === "function" ? fallback() : fallback;
}
return data;
}
export async function readSchedule() {
return readJson(schedulePath, (): ApiSchedule => ({
id: 111,
updatedAt: new Date().toISOString(),
}));
}
export async function writeSchedule(schedule: ApiSchedule) {
await writeFile(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
}
export async function readSubscriptions() {
let subscriptions = await readJson<ApiSubscription[]>(subscriptionsPath, []);
if (subscriptions.length && "keys" in subscriptions[0]) {
// Discard old format
subscriptions = [];
}
return subscriptions;
}
export async function writeSubscriptions(subscriptions: ApiSubscription[]) {
await writeFile(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
}
export async function readNextUserId() {
return await readJson(nextUserIdPath, 0);
}
export async function writeNextUserId(nextId: number) {
await writeFile(nextUserIdPath, String(nextId), "utf-8");
}
export async function nextUserId() {
let nextId = await readJson(nextUserIdPath, 0);
if (nextId === 0) {
nextId = Math.max(...(await readUsers()).map(user => user.id), -1) + 1;
}
await writeFile(nextUserIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function readUsers() {
return await readJson(usersPath, (): ServerUser[] => []);
}
export async function writeUsers(users: ServerUser[]) {
await writeFile(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
}
export async function readNextSessionId() {
return await readJson(nextSessionIdPath, 0);
}
export async function writeNextSessionId(nextId: number) {
await writeFile(nextSessionIdPath, String(nextId), "utf-8");
}
export async function nextSessionId() {
const nextId = await readJson(nextSessionIdPath, 0);
await writeFile(nextSessionIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function readSessions() {
return readJson<ServerSession[]>(sessionsPath, [])
}
export async function writeSessions(sessions: ServerSession[]) {
await writeFile(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
}
export async function nextAuthenticationMethodId() {
const nextId = await readJson(nextAuthenticationMethodIdPath, 0);
await writeFile(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function writeNextAuthenticationMethodId(nextId: number) {
await writeFile(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
}
export async function readAuthenticationMethods() {
return readJson<ServerAuthenticationMethod[]>(authMethodPath, [])
}
export async function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
await writeFile(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
}