Files
pgz-sport/routers/audit_seal_router.py
T
Damir Radulić 8fe2478b84 CC2 R3 frontend: login.html + admin_users.html (M1+M2+M10 UI)
- static/login.html: dark Palantir-style login with PGŽ branding,
  Prijava se / Zaboravljena lozinka, demo account quick-fills,
  GDPR cookie banner, autostore tokens (local/session)
- static/admin_users.html: full user-management admin panel:
  - Collapsible left sidebar (Pregled, Korisnici, Tenanti, Audit log,
    Sigurnost, GDPR, links to ERP/CRM)
  - Users table with filters (q, tenant, role, status, limit)
  - + Dodaj korisnika modal (CRUD via /api/admin/users/*)
  - Suspend / unsuspend / reset-password / delete actions
  - Audit log viewer + Security KPIs + GDPR queue
  - Self-service: change pwd, export data (Art. 20), erasure request (Art. 17)
- pgz_sport_api.py: /login and /admin/users URL routes
- auth/seed_demo.py: added tajnik@atletski.pgz.hr/Atl2026!,
  admin@ak-kvarner.hr/Kvarner2026! demo users

5/5 live tests pass: login JWT, /me, /admin/users, /gdpr/consent, /gdpr/export

Note: existing admin.html (CC4 ERP/OCR work) preserved intact;
admin_users.html is dedicated user-mgmt page linked from sidebar.
2026-05-05 00:20:03 +02:00

97 lines
3.2 KiB
Python

"""
audit_seal_router.py — HTTP surface for the Polygon PoS sealing module
Author: Damir Radulić (damir@rinet.one) / dradulic@outlook.com
Date: 2026-05-04
Endpoints (all under /sport/api):
POST /audit/seal
body: {
action: "sufinanciranje.approved",
ref_type: "sufinanciranje", (optional)
ref_id: "2026-001", (string or number)
payload: { ... }, (sha256 computed server-side)
data_hash: "abc..." (optional — if you already have the hash)
}
returns the seal record (seal_id, tx_hash, polygonscan_url, status, ...).
GET /audit/seal/list?action=&ref_type=&ref_id=&limit=
Recent seals for the audit-log UI.
GET /audit/seal/{seal_id}
Single seal with on-chain receipt cross-check (if web3 wired up).
The legacy hash-chain audit endpoints (/api/admin/audit-chain*) live in
pgz_sport_api.py and remain unchanged.
"""
from __future__ import annotations
import sys, os
from typing import Any, Optional
from fastapi import APIRouter, Body, HTTPException, Header
# blockchain.seal lives at /opt/pgz-sport/blockchain/seal.py
sys.path.insert(0, '/opt/pgz-sport')
from blockchain import seal as seal_mod # noqa: E402
router = APIRouter()
@router.post("/audit/seal")
def audit_seal(body: dict = Body(...),
x_user_email: Optional[str] = Header(default=None),
x_user_id: Optional[int] = Header(default=None)):
"""Seal an audit event to Polygon PoS (or queue for later if no key)."""
if not isinstance(body, dict):
raise HTTPException(400, "JSON body required")
action = (body.get('action') or '').strip()
if not action:
raise HTTPException(400, "action is required")
payload = body.get('payload')
data_hash = (body.get('data_hash') or '').strip().lower().lstrip('0x')
if not data_hash:
if payload is None:
raise HTTPException(400, "either data_hash or payload required")
data_hash = seal_mod.hash_payload(payload)
ref_id = body.get('ref_id')
if ref_id is None:
raise HTTPException(400, "ref_id is required")
ref_type = body.get('ref_type')
try:
result = seal_mod.seal_to_polygon(
data_hash=data_hash,
ref_id=str(ref_id),
action=action,
ref_type=ref_type,
payload=payload,
user_id=x_user_id,
user_email=x_user_email,
)
except ValueError as e:
raise HTTPException(400, str(e))
return result
@router.get("/audit/seal/list")
def audit_seal_list(action: Optional[str] = None,
ref_type: Optional[str] = None,
ref_id: Optional[str] = None,
limit: int = 50):
rows = seal_mod.list_seals(action=action, ref_type=ref_type,
ref_id=ref_id, limit=limit)
return {'count': len(rows), 'rows': rows,
'wallet': seal_mod.POLYGON_WALLET,
'chain_id': seal_mod.POLYGON_CHAIN_ID,
'live': seal_mod.HAS_WEB3 and bool(seal_mod.POLYGON_PRIVKEY)}
@router.get("/audit/seal/{seal_id}")
def audit_seal_get(seal_id: str):
row = seal_mod.verify_seal(seal_id)
if not row:
raise HTTPException(404, "seal not found")
return row