# Audit Log Manager — Implementation notes (review & debug) This document describes the **Audit Log Manager** delivered in this repo: database schema, backend recording, admin API, frontend layout, and how to verify end-to-end (Postgres, API, MinIO). ## 1. Database - **Migration:** `be0/migrations/008_audit_events.sql` - Creates PostgreSQL enum **`audit_action`** and table **`audit_events`** (append-only by convention). - Indexes: actor+time, entity+time, action+time, GIN on `metadata`. **Apply** (example against local Docker Postgres — adjust connection): ```bash psql "$INITIATIVE_DATABASE_URL" -f be0/migrations/008_audit_events.sql ``` (`INITIATIVE_DATABASE_URL` is typically `postgresql://…` for `psql`; the app uses SQLAlchemy async URL like `postgresql+asyncpg://…`.) Docker Compose mounts `008_audit_events.sql` into `docker-entrypoint-initdb.d` for **new** databases only. **Existing** `initiative_pg_data` volumes still need the `psql -f …/008_audit_events.sql` step once. ## 2. Backend model & helpers | Piece | Path | |-------|------| | ORM model | `be0/src/initiative_db/models.py` — `AuditEvent` | | Helpers | `be0/src/audit.py` — `record_audit`, `persist_audit_standalone`, `resolve_actor_fields` | - **`await record_audit(session, …)`** — insert via a **SAVEPOINT**: if `audit_events` is missing (migration not applied), logs a warning and **does not roll back** the parent transaction (so login/register still succeed). - **`await persist_audit_standalone(…)`** — own session + `commit`; same missing-table handling without raising. ## 3. Where events are written | Area | Actions / entity_type | Notes | |------|----------------------|--------| | Auth | `register` → `create` / `user` | Same transaction as user + roles | | Auth | OK login → `login` / `auth` | | | Auth | Bad login → `login_failed` / `auth` | **Standalone** insert after failed credentials | | Auth | Valid Bearer logout → `logout` / `auth` | **Standalone**; skipped if JWT does not decode | | Auth | Profile patch → `update` / `user` | Before/after: `fullName`, `phone` | | Auth | Password change → `update` / `user` | Snapshots `{ password: "[redacted|changed]" }` only | | Drafts autosave | `update` / `application_draft` | When `owner_id` is known (authenticated saves) | | MinIO evidence | `create`/`update`/`delete` / `application_evidence` | After DB + object write/delete in `main.py` | | Staff evidence review | `update` / `application_evidence_review` | | Admin adjudication (`application_admin_results.py`): - `create` / `application_admin_result` - `update` / … - `upsert` → `create` or `update` depending on prior row - `delete` — requires **`actor_user_id`**; `delete_admin_result(..., actor_user_id=…)` called from `main.py` Legacy table **`audit_log`** (draft telemetry) remains unchanged. ## 4. Admin HTTP API - **JWT decode** for admin routes (`decode_bearer_token`, `decode_access_token_user_id`) lives in **`be0/src/auth_jwt.py`** so audit routes do not pull in Argon2 at import time. - **Router:** `be0/src/admin_audit_routes.py`, mounted in `main.py` as **`/api/v1/admin`**. - **`GET /api/v1/admin/audit`** — list (admin JWT only). - Default window: **now − 7 days** → **now** if `from` / `to` omitted. - Query params: `from`, `to`, `actor_user_id`, `actor_email` **(exact, lowercased)** `entity_type`, `entity_id`, `action` (comma-separated), `request_id`, `page`, `page_size` (≤ 100), `sort` (`occurred_at:asc|desc`). - **`GET /api/v1/admin/audit/{id}`** — full row incl. `before` / `after` JSON. Non-admin receives **403**. ## 5. Frontend | Layer | Path | |-------|------| | Shared types + API client | `fe0/src/audit/` | | Admin UI | `fe0/src/admin/audit/` — `AuditLogManagerPage`, filters, table, detail **Sheet** | | Applicant-side copy (reuse) | `fe0/src/applicant/audit/actionLabels.ts` — Vietnamese labels for actions | | Nav | **`fe0/src/components/admin/DashboardSidebar.tsx`** (+ duplicate `DashboardSidebar.tsx`) — **« Nhật ký Audit »** → `/dashboard/admin/audit` | | Routes | `fe0/src/App.tsx` — **`admin/audit` before `admin`** nested under dashboard | Detail panel loads **`/api/v1/admin/audit/{id}`** and runs **`microdiff`** on the client only (see `audit/jsonMicrodiffLines.ts`), per architecture doc. Dependency: **`microdiff`** (added to `fe0/package.json`). ## 6. Debugging checklist 1. **500 on `/auth/login` after adding audit** — Postgres log / SQLAlchemy showed `relation "audit_events" does not exist`: apply migration `008`. Until then, **`record_audit`** skips the insert and auth still works. 2. **`ECONNREFUSED` from `fe0` to `:4402`** — API not listening yet (`be0` still on NLTK pip, or old entrypoint failed on `ollama: command not found`). Rebuild `be0`; entrypoint now **skips Ollama** when the binary is absent. Wait for `Uvicorn running on http://0.0.0.0:4402` before logging in. 3. **Table missing / Admin audit 503** — run migration `008_audit_events.sql`. 4. **Empty list but events expected** — check `from`/`to`; default is last **7 days** on the API. 5. **403 on `/api/v1/admin/audit`** — JWT must include **`admin`** in `roles` (same as other admin APIs). 6. **Failed-login rows missing** — must use **`persist_audit_standalone`** path; if Postgres URL wrong, standalone insert is skipped (`is_postgres_enabled()`). 7. **MinIO uploads without audit rows** — evidence handler must reach `upsert_evidence_artifact` **after** successful `s3.upload`; failures before that do not audit. 8. **Comment typo broke build once** — `formatAuditTime.ts` JSDoc used invalid `**/”` sequence; stick to **` */`** closing block comments. ## 7. Suggested verification flow 1. Apply migration → restart API. 2. Register / login → see `create`/`login` rows (filter `action=login,create`). 3. Save applicant draft tab → `application_draft`. 4. Upload evidence with MinIO configured → `application_evidence` with `metadata.minioBucket`. 5. Open **`/dashboard/admin/audit`** as admin → pagination + row click opens JSON + microdiff. ## 8. References - Spec: `assets/docs/audit-log-implementation.md` - Product plan: `docs/audit-log-manager-plan.md`