Add searchable admin audit history
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user