Files
Dune/tests/admin-access-fixture.ts
Quintine c2d392e155 Add administrator sign-in and private-safe room directory
Connect personal operator-provisioned admin keys, persisted roles and eight-hour sessions, current/all-session revocation and a filtered room directory. Additive schema protects final owner, revokes disabled accounts and records safe audit history. No game state changes; remaining administration scope stays explicit.

Verified: typecheck, lint, 5196 offline tests, 52 HTTP tests, production build; authenticated local HTTP and desktop/mobile browser checks. Independent privacy/security review fixed malformed nested roster searches and read-time session revocation. Preserved 1,671 prior rooms and 3,538 seats. Container/deployed acceptance follows publication.
2026-09-22 09:36:11 +10:00

94 lines
2.9 KiB
TypeScript

import { createHash, randomBytes, randomUUID } from 'node:crypto';
import { readFileSync, readdirSync } from 'node:fs';
import { DatabaseSync } from 'node:sqlite';
import type { AdminRole } from '../db/admin-access';
export function adminStore(path = ':memory:', migrate = true) {
const sqlite = new DatabaseSync(path);
sqlite.exec('PRAGMA foreign_keys = ON');
if (migrate) {
for (const file of readdirSync(new URL('../drizzle/', import.meta.url))
.filter((file) => file.endsWith('.sql'))
.sort())
sqlite.exec(
readFileSync(new URL('../drizzle/' + file, import.meta.url), 'utf8'),
);
}
const hooks: { beforeBatch?: () => Promise<void> } = {};
class Statement {
values: (string | number | null)[] = [];
constructor(readonly sql: string) {}
bind(...values: (string | number | null)[]) {
this.values = values;
return this;
}
async first() {
return sqlite.prepare(this.sql).get(...this.values) ?? null;
}
async all() {
return {
results: sqlite.prepare(this.sql).all(...this.values),
success: true,
};
}
async run() {
const statement = sqlite.prepare(this.sql);
if (statement.columns().length) {
return {
results: statement.all(...this.values),
meta: { changes: 0 },
success: true,
};
}
const result = statement.run(...this.values);
return {
results: [],
meta: { changes: Number(result.changes) },
success: true,
};
}
}
const database = {
prepare: (sql: string) => new Statement(sql),
batch: async (statements: Statement[]) => {
await hooks.beforeBatch?.();
sqlite.exec('BEGIN');
try {
const results = [];
for (const statement of statements) results.push(await statement.run());
sqlite.exec('COMMIT');
return results;
} catch (error) {
sqlite.exec('ROLLBACK');
throw error;
}
},
} as unknown as D1Database;
function provision(role: AdminRole = 'owner', name = 'Admin') {
const id = randomUUID();
const key = `dune-admin.${id}.${randomBytes(32).toString('hex')}`;
sqlite
.prepare(`INSERT INTO admin_accounts
(id,name,role,key_hash,created_at,updated_at) VALUES (?,?,?,?,?,?)`)
.run(id, name, role, sha256(key), 1000, 1000);
return { id, key, name, role };
}
return { sqlite, database, hooks, provision };
}
export const sha256 = (value: string) =>
createHash('sha256').update(value).digest('hex');
export function pauseAdminBatch(store: ReturnType<typeof adminStore>) {
let entered!: () => void;
let release!: () => void;
const waiting = new Promise<void>((resolve) => (entered = resolve));
const gate = new Promise<void>((resolve) => (release = resolve));
store.hooks.beforeBatch = async () => {
delete store.hooks.beforeBatch;
entered();
await gate;
};
return { waiting, release };
}