feat(audit-log): add configurable retention, export, Auditor role, and enhanced filtering (#258)

- Configurable retention: audit_retention_days setting (1-365 days, default 90)
  replaces hardcoded 90-day retention, exposed in Settings > Data Retention
- Export: one-click CSV/JSON export of filtered audit data via new
  GET /api/audit-log/export endpoint (capped at 10,000 entries)
- Auditor role: read-only role with system:audit permission for viewing
  and exporting audit logs without admin privileges (Admiral tier)
- Enhanced filtering: full-text search across summaries/paths/usernames,
  date range picker, and expandable row details showing request path,
  IP address, node ID, and entry ID
This commit is contained in:
Anso
2026-03-29 20:18:51 -04:00
committed by GitHub
parent f4428a394c
commit d586ce393a
11 changed files with 289 additions and 65 deletions
+7 -1
View File
@@ -62,7 +62,7 @@ export interface WebhookExecution {
export type AuthProvider = 'local' | 'ldap' | 'oidc_google' | 'oidc_github' | 'oidc_okta';
export type UserRole = 'admin' | 'viewer' | 'deployer' | 'node-admin';
export type UserRole = 'admin' | 'viewer' | 'deployer' | 'node-admin' | 'auditor';
export type ResourceType = 'stack' | 'node';
export interface User {
@@ -1109,6 +1109,7 @@ export class DatabaseService {
method?: string;
from?: number;
to?: number;
search?: string;
} = {}): { entries: AuditLogEntry[]; total: number } {
const page = filters.page ?? 1;
const limit = filters.limit ?? 50;
@@ -1133,6 +1134,11 @@ export class DatabaseService {
conditions.push('timestamp <= ?');
params.push(filters.to);
}
if (filters.search) {
conditions.push('(summary LIKE ? OR path LIKE ? OR username LIKE ?)');
const term = `%${filters.search}%`;
params.push(term, term, term);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
+2 -1
View File
@@ -304,7 +304,8 @@ export class MonitorService {
db.cleanupOldMetrics(isNaN(retentionHours) ? 24 : retentionHours);
const retentionDays = parseInt(settings['log_retention_days'] || '30', 10);
db.cleanupOldNotifications(isNaN(retentionDays) ? 30 : retentionDays);
db.cleanupOldAuditLogs(90);
const auditRetentionDays = parseInt(settings['audit_retention_days'] || '90', 10);
db.cleanupOldAuditLogs(isNaN(auditRetentionDays) ? 90 : auditRetentionDays);
} catch (e) {
console.error('MonitorService: failed to cleanup old data', e);
}