Implement database administration

Add routes and admin panel elements for creating a database backup,
restoring from a backup, deleting the existing schedule, and replacing
the database with the demo schedule.  These server as crude ways to
manage the data stored in the system.
This commit is contained in:
Hornwitser 2025-06-28 01:23:52 +02:00
parent b2f48e98e0
commit e5e923bc8d
6 changed files with 190 additions and 5 deletions

View file

@ -0,0 +1,15 @@
import { readNextSessionId, readNextUserId, readSchedule, readSessions, readSubscriptions, readUsers } from "~/server/database";
export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event);
setHeader(event, "Content-Disposition", 'attachment; filename="database-dump.json"');
setHeader(event, "Content-Type", "application/json; charset=utf-8");
return {
nextUserId: await readNextUserId(),
users: await readUsers(),
nextSessionId: await readNextSessionId(),
sessions: await readSessions(),
subscriptions: await readSubscriptions(),
schedule: await readSchedule(),
};
})

View file

@ -0,0 +1,7 @@
import { writeSchedule, writeUsers } from "~/server/database";
import { generateDemoSchedule, generateDemoAccounts } from "~/server/generate-demo-schedule";
export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event);
await writeUsers(generateDemoAccounts());
await writeSchedule(generateDemoSchedule());
})

View file

@ -0,0 +1,44 @@
import { readNextSessionId, readNextUserId, readSessions, type ServerSession, type ServerUser, writeNextSessionId, writeNextUserId, writeSchedule, writeSessions, writeSubscriptions, writeUsers } from "~/server/database";
import type { ApiSchedule, ApiSubscription } from "~/shared/types/api";
export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event);
const formData = await readMultipartFormData(event);
let snapshot: undefined | {
nextUserId: number,
users: ServerUser[],
nextSessionId: number,
sessions: ServerSession[],
subscriptions: ApiSubscription[],
schedule: ApiSchedule,
};
for (const part of formData ?? []) {
if (part.name === "snapshot") {
snapshot = JSON.parse(part.data.toString("utf-8"));
}
}
if (!snapshot) {
throw createError({
statusCode: 400,
statusMessage: "Bad Request",
message: "snapshot missing."
});
}
const currentNextUserId = await readNextUserId();
await writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId));
await writeUsers(snapshot.users);
const currentNextSessionId = await readNextSessionId();
await writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId));
const currentSessions = new Map((await readSessions()).map(session => [session.id, session]));
await writeSessions(snapshot.sessions.filter(session => {
const current = currentSessions.get(session.id);
// Only keep sessions that match the account id in both sets to avoid
// resurrecting deleted sessions. This will still cause session cross
// pollution if a snapshot from another instance is loaded here.
return current && current.account.id === session.account.id;
}));
await writeSubscriptions(snapshot.subscriptions);
await writeSchedule(snapshot.schedule);
await sendRedirect(event, "/");
})

View file

@ -0,0 +1,11 @@
import { writeSchedule } from "~/server/database";
import type { ApiSchedule } from "~/shared/types/api";
export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event);
const schedule: ApiSchedule = {
id: 111,
updatedAt: new Date().toISOString(),
};
await writeSchedule(schedule);
})

View file

@ -1,6 +1,5 @@
import { readFile, unlink, writeFile } from "node:fs/promises";
import type { ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api";
import { generateDemoSchedule, generateDemoAccounts } from "~/server/generate-demo-schedule";
import type { Id } from "~/shared/types/common";
export interface ServerSession {
@ -65,7 +64,10 @@ async function readJson<T>(filePath: string, fallback: T) {
}
export async function readSchedule() {
return readJson(schedulePath, generateDemoSchedule);
return readJson(schedulePath, (): ApiSchedule => ({
id: 111,
updatedAt: new Date().toISOString(),
}));
}
export async function writeSchedule(schedule: ApiSchedule) {
@ -85,6 +87,14 @@ 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) {
@ -95,13 +105,21 @@ export async function nextUserId() {
}
export async function readUsers() {
return await readJson(usersPath, generateDemoAccounts as () => ServerUser[]);
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");