feat(audit): admin audit log (Notification P3)
Append-only who/what/when trail for admin actions + scheduler run summaries, generalizing QuotaEvent. Logged at ACTION / run-summary granularity — never per-row (a scheduler run touching thousands of videos is ONE row). Backend: - migration 0054_audit_log + AuditLog model (actor_id FK SET NULL, action, target_type/id, summary, before/after JSON, created_at). - app/audit.py: AuditAction constants (single source of truth, i18n'd on the client) + record() (adds to the caller's txn) + gc(retention_days). - producers wired: role change / suspend / delete / invite approve-deny-add / demo add-remove-reset / discovery purge (admin.py); config set/reset — secret VALUES never logged, key only (config.py); scheduler interval + maintenance tune (scheduler routes); sync pause/resume (sync.py); and the scheduler _job() choke point writes ONE run-summary row per run — manual runs + errors always, automatic runs only when they changed something (no no-op-poll spam). - retention: audit_gc scheduler job prunes rows older than audit_retention_days (sysconfig, default 180; 0 = keep forever). - admin API routes/audit.py (/api/admin/audit): recent-window list (actor emails resolved, System for background) + clear (leaves one tamper-evidence row). Frontend: - new admin-only "Audit log" nav page (ScrollText) using the shared DataTable (first admin page to reuse it) — sort/filter/paginate/persist, action select filter, actor text filter, before→after detail, Clear-all with confirm. - AuditLog.tsx + auditColumns.tsx + api.adminAudit/clearAudit + AuditRow type. - trilingual audit + auditActions namespaces (HU/EN/DE) + nav + config-group + scheduler job labels; Audit config group (retention days).
This commit is contained in:
parent
c1d38eea21
commit
318fdc4812
35 changed files with 720 additions and 29 deletions
|
|
@ -3,8 +3,10 @@ app.sysconfig. Secret values (e.g. SMTP password) are write-only — never retur
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import audit
|
||||
from app import email as email_mod
|
||||
from app import sysconfig
|
||||
from app.audit import AuditAction
|
||||
from app.db import get_db
|
||||
from app.models import User
|
||||
from app.routes.admin import admin_user
|
||||
|
|
@ -21,7 +23,7 @@ def get_config(_: User = Depends(admin_user), db: Session = Depends(get_db)) ->
|
|||
def set_config(
|
||||
key: str,
|
||||
payload: dict,
|
||||
_: User = Depends(admin_user),
|
||||
admin: User = Depends(admin_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
s = sysconfig.spec(key)
|
||||
|
|
@ -34,22 +36,40 @@ def set_config(
|
|||
status_code=400,
|
||||
detail="Set TOKEN_ENCRYPTION_KEY to store secrets in the database.",
|
||||
)
|
||||
old = None if s.secret else sysconfig.get(db, key)
|
||||
try:
|
||||
sysconfig.set_value(db, key, payload["value"])
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc) or "Invalid value")
|
||||
# Never log a secret's value — only that it was changed, by whom.
|
||||
new = None if s.secret else sysconfig.get(db, key)
|
||||
audit.record(
|
||||
db, admin.id, AuditAction.CONFIG_SET, target_type="config", target_id=key,
|
||||
summary=f"{key} = (secret updated)" if s.secret else f"{key} = {new}",
|
||||
before=None if s.secret else {"value": old},
|
||||
after=None if s.secret else {"value": new},
|
||||
)
|
||||
db.commit()
|
||||
return sysconfig.describe(db)
|
||||
|
||||
|
||||
@router.delete("/{key}")
|
||||
def reset_config(
|
||||
key: str,
|
||||
_: User = Depends(admin_user),
|
||||
admin: User = Depends(admin_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
if sysconfig.spec(key) is None:
|
||||
s = sysconfig.spec(key)
|
||||
if s is None:
|
||||
raise HTTPException(status_code=404, detail="Unknown config key")
|
||||
old = None if s.secret else sysconfig.get(db, key)
|
||||
sysconfig.reset(db, key)
|
||||
audit.record(
|
||||
db, admin.id, AuditAction.CONFIG_RESET, target_type="config", target_id=key,
|
||||
summary=f"{key} → default",
|
||||
before=None if s.secret else {"value": old},
|
||||
)
|
||||
db.commit()
|
||||
return sysconfig.describe(db)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue