2026-06-19 12:22:36 +02:00
|
|
|
"""Admin Configuration page API: read/edit the DB-overridable operational settings defined in
|
|
|
|
|
app.sysconfig. Secret values (e.g. SMTP password) are write-only — never returned."""
|
2026-07-12 07:47:07 +02:00
|
|
|
import re
|
|
|
|
|
|
2026-06-19 12:22:36 +02:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
from app import audit
|
2026-06-19 12:22:36 +02:00
|
|
|
from app import email as email_mod
|
|
|
|
|
from app import sysconfig
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
from app.audit import AuditAction
|
2026-06-19 12:22:36 +02:00
|
|
|
from app.db import get_db
|
|
|
|
|
from app.models import User
|
|
|
|
|
from app.routes.admin import admin_user
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/admin/config", tags=["admin"])
|
|
|
|
|
|
2026-07-12 07:47:07 +02:00
|
|
|
# Strip URL userinfo (user:pass@) so a non-secret setting that happens to embed credentials —
|
|
|
|
|
# e.g. an authenticated egress proxy http://user:pass@host:port — never lands plaintext in the
|
|
|
|
|
# audit trail. Non-secret values are shown live in the UI, but the log shouldn't retain creds.
|
|
|
|
|
_URL_USERINFO = re.compile(r"(://)[^/@\s]+@")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _redact(v):
|
|
|
|
|
return _URL_USERINFO.sub(r"\1***@", v) if isinstance(v, str) else v
|
|
|
|
|
|
2026-06-19 12:22:36 +02:00
|
|
|
|
|
|
|
|
@router.get("")
|
|
|
|
|
def get_config(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
|
|
|
|
return sysconfig.describe(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{key}")
|
|
|
|
|
def set_config(
|
|
|
|
|
key: str,
|
|
|
|
|
payload: dict,
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
admin: User = Depends(admin_user),
|
2026-06-19 12:22:36 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> dict:
|
|
|
|
|
s = sysconfig.spec(key)
|
|
|
|
|
if s is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Unknown config key")
|
|
|
|
|
if "value" not in payload:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Missing 'value'")
|
|
|
|
|
if s.secret and not sysconfig.secrets_manageable():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="Set TOKEN_ENCRYPTION_KEY to store secrets in the database.",
|
|
|
|
|
)
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
old = None if s.secret else sysconfig.get(db, key)
|
2026-06-19 12:22:36 +02:00
|
|
|
try:
|
|
|
|
|
sysconfig.set_value(db, key, payload["value"])
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(exc) or "Invalid value")
|
2026-07-12 07:47:07 +02:00
|
|
|
# Never log a secret's value — only that it was changed, by whom. Non-secret values are logged
|
|
|
|
|
# but with URL credentials redacted (a proxy/URL setting can embed user:pass). Skip the row when
|
|
|
|
|
# a non-secret value didn't actually change (a no-op re-save shouldn't add trail noise); secrets
|
|
|
|
|
# are write-only so we can't compare — always log.
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
new = None if s.secret else sysconfig.get(db, key)
|
2026-07-12 07:47:07 +02:00
|
|
|
if s.secret or new != old:
|
|
|
|
|
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} = {_redact(new)}",
|
|
|
|
|
before=None if s.secret else {"value": _redact(old)},
|
|
|
|
|
after=None if s.secret else {"value": _redact(new)},
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
2026-06-19 12:22:36 +02:00
|
|
|
return sysconfig.describe(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{key}")
|
|
|
|
|
def reset_config(
|
|
|
|
|
key: str,
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
admin: User = Depends(admin_user),
|
2026-06-19 12:22:36 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> dict:
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
s = sysconfig.spec(key)
|
|
|
|
|
if s is None:
|
2026-06-19 12:22:36 +02:00
|
|
|
raise HTTPException(status_code=404, detail="Unknown config key")
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
old = None if s.secret else sysconfig.get(db, key)
|
2026-06-19 12:22:36 +02:00
|
|
|
sysconfig.reset(db, key)
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
audit.record(
|
|
|
|
|
db, admin.id, AuditAction.CONFIG_RESET, target_type="config", target_id=key,
|
|
|
|
|
summary=f"{key} → default",
|
2026-07-12 07:47:07 +02:00
|
|
|
before=None if s.secret else {"value": _redact(old)},
|
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).
2026-07-12 07:32:41 +02:00
|
|
|
)
|
|
|
|
|
db.commit()
|
2026-06-19 12:22:36 +02:00
|
|
|
return sysconfig.describe(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/test-email")
|
|
|
|
|
def send_test_email(
|
|
|
|
|
user: User = Depends(admin_user),
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""Send a test email to the requesting admin using the current (DB or env) SMTP config —
|
|
|
|
|
lets the admin verify the settings actually work."""
|
|
|
|
|
if not email_mod.email_enabled():
|
|
|
|
|
raise HTTPException(status_code=400, detail="SMTP is not configured.")
|
|
|
|
|
sent = email_mod.send_test(user.email)
|
|
|
|
|
if not sent:
|
|
|
|
|
raise HTTPException(status_code=422, detail="SMTP send failed — check the settings.")
|
|
|
|
|
return {"sent": True, "to": user.email}
|