From 80d82d36dd721d2208dd2590d8dc6bb817f83bef Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Thu, 28 May 2026 11:49:59 +0200 Subject: [PATCH] audit logging --- app/db/audit.py | 77 +++++++++++++++++++++++++++++++++++++++++ app/main.py | 3 +- app/routes/apps.py | 41 ++++++++++++++++++++++ app/routes/audit.py | 61 ++++++++++++++++++++++++++++++++ app/routes/auth.py | 16 +++++++++ app/routes/backups.py | 34 ++++++++++++++++++ app/templates/layout.py | 1 + 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 app/db/audit.py create mode 100644 app/routes/audit.py diff --git a/app/db/audit.py b/app/db/audit.py new file mode 100644 index 0000000..4b7a40d --- /dev/null +++ b/app/db/audit.py @@ -0,0 +1,77 @@ +import json +from typing import Any + +from app.db.database import get_connection + + +def log_audit_event( + user, + action: str, + target_type: str, + target_id=None, + source: str = "portal", + metadata: dict[str, Any] | None = None, +): + if user: + user_id = user.get("id") + username = user.get("username") or "system" + else: + user_id = None + username = "system" + source = "system" + + metadata_json = json.dumps(metadata or {}, ensure_ascii=False, sort_keys=True) + + con = get_connection() + con.execute( + """ + INSERT INTO audit_events ( + user_id, + username, + action, + target_type, + target_id, + source, + metadata, + created_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + """, + ( + user_id, + username, + action, + target_type, + target_id, + source, + metadata_json, + ), + ) + con.commit() + con.close() + + +def get_audit_events(limit: int = 200): + con = get_connection() + + rows = con.execute( + """ + SELECT + id, + user_id, + username, + action, + target_type, + target_id, + source, + metadata, + created_at + FROM audit_events + ORDER BY id DESC + LIMIT ? + """, + (limit,), + ).fetchall() + + con.close() + return [dict(row) for row in rows] diff --git a/app/main.py b/app/main.py index a36118d..4ac1d1f 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from .config import read_env_value -from .routes import apps, auth, backups, deployments, health +from .routes import apps, audit, auth, backups, deployments, health def create_app() -> FastAPI: @@ -27,6 +27,7 @@ def create_app() -> FastAPI: app.include_router(apps.router) app.include_router(backups.router) app.include_router(deployments.router) + app.include_router(audit.router) return app diff --git a/app/routes/apps.py b/app/routes/apps.py index e74eadd..519e275 100644 --- a/app/routes/apps.py +++ b/app/routes/apps.py @@ -14,6 +14,7 @@ from ..config import ( read_env_value, ) from ..db.apps import get_apps, update_app_resources +from ..db.audit import log_audit_event from ..shell import run_command from ..templates.layout import page, render_result @@ -203,6 +204,20 @@ def create_app( deploy_result = run_command([DEPLOY_SCRIPT, app_id]) status = "OK" if create_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED" + log_audit_event( + user, + action="create_app", + target_type="app", + target_id=app_id, + metadata={ + "app_id": app_id, + "app_name": app_name, + "template": template, + "status": status, + "create_returncode": create_result.returncode, + "deploy_returncode": deploy_result.returncode, + }, + ) return render_result( title=f"Vytvoření aplikace: {status}", @@ -223,6 +238,17 @@ def create_app( def delete_app(app_id: str = Form(...), user=Depends(require_user)): result = run_command([DELETE_APP_SCRIPT, app_id]) status = "OK" if result.returncode == 0 else "FAILED" + log_audit_event( + user, + action="delete_app", + target_type="app", + target_id=app_id, + metadata={ + "app_id": app_id, + "status": status, + "returncode": result.returncode, + }, + ) return render_result( title=f"Smazání aplikace: {status}", @@ -253,6 +279,21 @@ def update_resources( if catalog_result.returncode == 0 and compose_result.returncode == 0 and deploy_result.returncode == 0 else "FAILED" ) + log_audit_event( + user, + action="update_resources", + target_type="app", + target_id=app_id, + metadata={ + "app_id": app_id, + "memory": memory, + "cpus": cpus, + "status": status, + "catalog_returncode": catalog_result.returncode, + "compose_returncode": compose_result.returncode, + "deploy_returncode": deploy_result.returncode, + }, + ) return render_result( title=f"Úprava prostředků: {status}", diff --git a/app/routes/audit.py b/app/routes/audit.py new file mode 100644 index 0000000..ceb638b --- /dev/null +++ b/app/routes/audit.py @@ -0,0 +1,61 @@ +import html + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from app.auth import require_user +from app.db.audit import get_audit_events +from app.templates.layout import page + +router = APIRouter() + + +@router.get("/audit", response_class=HTMLResponse) +def audit_page(request: Request, user=Depends(require_user)): + rows = "" + + for event in get_audit_events(): + metadata = event.get("metadata") or "{}" + metadata_preview = metadata if len(metadata) <= 180 else f"{metadata[:177]}..." + + rows += f""" +
{html.escape(metadata_preview)}Přehled interních akcí provedených v portálu.
+| Čas | +Uživatel | +Akce | +Typ cíle | +Cíl | +Zdroj | +Metadata | +
|---|