Add searchable admin audit history

This commit is contained in:
Jonathan Sykes
2026-10-14 01:54:08 +08:00
parent 6bcaccbcaf
commit 301fab335f
9 changed files with 208 additions and 4 deletions

View File

@@ -44,6 +44,7 @@ import { join } from 'node:path';
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
import { registerAnalyticsRoutes } from './admin-analytics.js';
import { registerTranscriptionRoutes } from './transcriptions.js';
import { audit, registerAuditRoutes } from './audit-log.js';
export const NOTE_KINDS = new Set(['lyrics', 'chapters']);
// A YouTube id or one of the server's own uploads (see uploads.js).
@@ -507,6 +508,7 @@ export function registerNoteRoutes(app, deps) {
if (!r.lines.length) return c.json({ ok: false, error: 'this video has no usable captions' }, 404);
const data = sanitizeLyrics({ lines: r.lines.map((l) => ({ ...l, kind: 'line' })), tags: [], offset: 0 });
const saved = await db.saveNote({ videoId: id, kind: 'lyrics', data, source: 'auto', updatedBy: who.by, force: true });
await audit(db, who.by, 'lyrics.save.auto-captions', id, { rev: saved.rev, lines: data.lines.length });
return c.json({ ok: true, rev: saved.rev, lines: data.lines.length, lang: r.lang, auto: r.auto });
} catch (err) {
return c.json({ ok: false, error: err.message }, 502);
@@ -551,6 +553,7 @@ export function registerNoteRoutes(app, deps) {
offset: 0,
});
const saved = await db.saveNote({ videoId: id, kind: 'lyrics', data, source: 'auto', updatedBy: who.by, force: true });
await audit(db, who.by, 'lyrics.save.lrclib', id, { rev: saved.rev, lines: data.lines.length });
return c.json({ ok: true, rev: saved.rev, lines: data.lines.length, synced: hit.synced, match: { track: hit.meta.trackName, artist: hit.meta.artistName, duration: hit.meta.duration } });
} catch (err) {
return c.json({ ok: false, error: err.message }, 502);
@@ -578,6 +581,7 @@ export function registerNoteRoutes(app, deps) {
});
if (!r.ok) return c.json({ ok: false, error: 'someone else saved a newer version', conflict: true, current: r.current }, 409);
if (kind === 'lyrics') autoResolve(id, data);
await audit(db, who.by, `notes.save.${kind}`, id, { rev: r.rev, source: who.via });
return c.json({ ok: true, rev: r.rev, data });
} catch (err) {
return c.json({ ok: false, error: err.message }, 500);
@@ -658,11 +662,13 @@ export function registerNoteRoutes(app, deps) {
const token = newApiToken();
const id = randomBytes(6).toString('hex');
await db.createApiToken({ id, label, tokenHash: hashToken(token) });
await audit(db, 'admin', 'token.create', id, { label });
return c.json({ ok: true, id, label, token });
});
app.delete('/api/admin/tokens/:tid', requireAdmin, async (c) => {
const ok = await db.deleteApiToken(c.req.param('tid'));
if (ok) await audit(db, 'admin', 'token.revoke', c.req.param('tid'));
return ok ? c.json({ ok: true }) : c.json({ ok: false, error: 'not found' }, 404);
});
@@ -673,6 +679,8 @@ export function registerNoteRoutes(app, deps) {
await next();
};
registerAuditRoutes(app, { db, adminAuth: requireAdminOrToken });
if (deps.analytics) registerAnalyticsRoutes(app, { adminAuth: requireAdminOrToken, runYtdlp, ...deps.analytics });
registerTranscriptionRoutes(app, {
@@ -707,7 +715,11 @@ export function registerNoteRoutes(app, deps) {
app.post('/api/admin/media/:id/redownload', requireAdminOrToken, async c => {
if (!media) return c.json({ ok: false, error: 'media cache unavailable' }, 503);
try { return c.json({ ok: true, ...(await media.redownload(c.req.param('id'))) }, 202); }
try {
const result = await media.redownload(c.req.param('id'));
await audit(db, 'admin', 'media.redownload', c.req.param('id'), result);
return c.json({ ok: true, ...result }, 202);
}
catch (err) { return c.json({ ok: false, error: err.message }, 400); }
});
@@ -727,6 +739,7 @@ export function registerNoteRoutes(app, deps) {
updatedBy: `admin (rev ${old.rev})`, force: true,
});
if (kind === 'lyrics') autoResolve(id, restored);
await audit(db, 'admin', `notes.restore.${kind}`, id, { fromRev: old.rev, newRev: r.rev });
return c.json({ ok: true, rev: r.rev });
});
@@ -763,11 +776,14 @@ export function registerNoteRoutes(app, deps) {
try { body = await c.req.json(); } catch { /* status required below */ }
if (!Number.isInteger(fid) || !['open', 'resolved'].includes(body.status)) return c.json({ ok: false, error: 'bad request' }, 400);
const ok = await db.setFlagStatus(fid, body.status, 'admin');
if (ok) await audit(db, 'admin', `report.${body.status}`, String(fid));
return ok ? c.json({ ok: true, open: await db.countOpenFlags() }) : c.json({ ok: false, error: 'not found' }, 404);
});
app.delete('/api/admin/flags/:fid', requireAdmin, async (c) => {
const ok = await db.deleteFlag(Number(c.req.param('fid')));
const id = Number(c.req.param('fid'));
const ok = await db.deleteFlag(id);
if (ok) await audit(db, 'admin', 'report.delete', String(id));
return ok ? c.json({ ok: true, open: await db.countOpenFlags() }) : c.json({ ok: false, error: 'not found' }, 404);
});