A check of every write path found gaps in what the audit log captured: - Sign-ins and sign-outs are now recorded with the IP they came from (sign-out is recorded first and can't block signing out). - A new account is recorded when it's created on first sign-in, including when the very first user becomes admin - so the log shows who gained access, not only who changed things. - The automatic log purge, which deletes audit entries, now records itself, attributed to "system". recordAudit() takes an optional actor for this. It only records when something was actually deleted. - Settings updates record what changed (before and after) instead of only which sections were touched. The notification channels (Gotify, ntfy, SMTP, webhook) record field names only: they hold credentials, and webhook URLs and public ntfy topics act as secrets, while the audit log is readable by operators and Settings is admin-only. - Integration edits record renames, enabling/disabling, whether credentials were replaced (never the credentials), and which settings fields changed (names only). The Privacy page and README now say sign-ins store an IP in the audit log. Verified through the real routes against a scratch database: user creation, the logout route, the automatic purge, settings and integration edits - including that a secret token and a webhook URL appear nowhere in the stored entries. The sign-in callback itself needs a real identity provider and wasn't run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
57 lines
2.4 KiB
TypeScript
57 lines
2.4 KiB
TypeScript
import { getSettings, getInternalFlag, setInternalFlag } from "./settingsStore.js";
|
|
import { purgeOldLogs } from "./logRetention.js";
|
|
import { recordAudit } from "./audit.js";
|
|
|
|
const LAST_RUN_FLAG = "logRetentionLastRunAt";
|
|
|
|
async function runPurge(): Promise<void> {
|
|
const { logRetention } = await getSettings();
|
|
if (!logRetention.enabled) return;
|
|
const result = await purgeOldLogs(logRetention.retentionDays);
|
|
await setInternalFlag(LAST_RUN_FLAG, new Date().toISOString());
|
|
if (result.diagDeleted || result.auditDeleted) {
|
|
// Trimming the audit log is itself something to be able to look back on — attributed to the system, since no one asked for it.
|
|
await recordAudit({
|
|
category: "settings",
|
|
action: "purge_logs",
|
|
detail: { automatic: true, retentionDays: logRetention.retentionDays, ...result },
|
|
});
|
|
console.log(
|
|
`[logRetention] purged ${result.diagDeleted} diagnostic log and ${result.auditDeleted} audit log entries older than ${logRetention.retentionDays} days`,
|
|
);
|
|
}
|
|
}
|
|
|
|
let currentTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
/** (Re)arms the periodic purge timer per the current settings. Call again after settings change. */
|
|
export async function scheduleLogRetentionPurge(): Promise<void> {
|
|
if (currentTimer) {
|
|
clearInterval(currentTimer);
|
|
currentTimer = null;
|
|
}
|
|
const { logRetention } = await getSettings();
|
|
if (!logRetention.enabled) {
|
|
console.log("[logRetention] automatic purge disabled");
|
|
return;
|
|
}
|
|
const intervalMs = logRetention.intervalHours * 60 * 60 * 1000;
|
|
currentTimer = setInterval(() => {
|
|
runPurge().catch((err) => console.error("[logRetention] purge failed:", err));
|
|
}, intervalMs);
|
|
console.log(`[logRetention] automatic purge scheduled every ${logRetention.intervalHours}h, keeping ${logRetention.retentionDays} days`);
|
|
}
|
|
|
|
/** Runs immediately at startup if a purge is overdue (e.g. the server was down past the interval), then arms the periodic timer. */
|
|
export async function initLogRetentionScheduler(): Promise<void> {
|
|
const { logRetention } = await getSettings();
|
|
if (logRetention.enabled) {
|
|
const lastRun = await getInternalFlag(LAST_RUN_FLAG);
|
|
const dueAt = lastRun ? new Date(lastRun).getTime() + logRetention.intervalHours * 60 * 60 * 1000 : 0;
|
|
if (Date.now() >= dueAt) {
|
|
await runPurge().catch((err) => console.error("[logRetention] purge failed:", err));
|
|
}
|
|
}
|
|
await scheduleLogRetentionPurge();
|
|
}
|