67372d6c58
- auth/gdpr.py: dodan @me_router.post('/request-deletion') alias
koji proxy-a na request_erasure (Art. 17). Koristi pravi EraseReq pydantic.
- static/app.html: obrisana placeholder profileDeleteAccount funkcija
na liniji 944 (M10 mock alert) — sada samo real implementacija na 1902.
- E2E verified: damir@pgz.hr → POST /users/me/request-deletion → 200,
DB row pgz_sport.gdpr_erasure_requests #1 pending.
Tag: P0-demo-fix
154 lines
6.7 KiB
HTML
154 lines
6.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="hr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Audit Log — PGŽ Sport</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<link rel="icon" href="data:,">
|
|
<style>
|
|
:root { --bg0:#08090e; --bg1:#11141d; --bg2:#1a1f2c; --txt:#e6e9ef; --muted:#7a8294;
|
|
--pgz-blue:#003087; --pgz-gold:#F4C430; --green:#1a8754; --red:#dc3545; --orange:#fd7e14; }
|
|
* { box-sizing:border-box; margin:0; padding:0; }
|
|
body { font-family:'Inter',system-ui,sans-serif; background:var(--bg0); color:var(--txt); padding:24px; line-height:1.5; }
|
|
h1 { color:var(--pgz-gold); margin-bottom:6px; }
|
|
.sub { color:var(--muted); margin-bottom:24px; }
|
|
.toolbar { display:flex; gap:12px; margin-bottom:18px; flex-wrap:wrap; }
|
|
.btn { background:var(--pgz-blue); color:white; border:none; padding:9px 16px; border-radius:6px; cursor:pointer; font-weight:500; }
|
|
.btn:hover { background:#0040b8; }
|
|
.btn.secondary { background:var(--bg2); }
|
|
input,select { background:var(--bg2); color:var(--txt); border:1px solid #2a3144; padding:9px 12px; border-radius:6px; min-width:160px; }
|
|
table { width:100%; border-collapse:collapse; background:var(--bg1); border-radius:8px; overflow:hidden; margin-top:8px; }
|
|
th { background:var(--bg2); padding:12px; text-align:left; color:var(--pgz-gold); font-size:0.85rem; text-transform:uppercase; }
|
|
td { padding:11px 12px; border-top:1px solid #1a1f2c; font-size:0.92rem; }
|
|
tr:hover { background:#13182a; }
|
|
.badge { padding:3px 9px; border-radius:11px; font-size:0.75rem; font-weight:600; }
|
|
.b-create { background:rgba(26,135,84,0.2); color:#7fdca5; }
|
|
.b-update { background:rgba(253,126,20,0.2); color:#ffaa66; }
|
|
.b-delete { background:rgba(220,53,69,0.2); color:#ff7e85; }
|
|
.b-seal { background:rgba(244,196,48,0.2); color:var(--pgz-gold); }
|
|
.tx-link { color:#5fa8d3; text-decoration:none; font-family:monospace; font-size:0.85rem; }
|
|
.tx-link:hover { text-decoration:underline; }
|
|
.empty { padding:60px; text-align:center; color:var(--muted); }
|
|
.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin-bottom:18px; }
|
|
.stat { background:var(--bg1); padding:14px; border-radius:8px; border-left:3px solid var(--pgz-blue); }
|
|
.stat .v { font-size:1.6rem; font-weight:700; color:var(--pgz-gold); }
|
|
.stat .l { color:var(--muted); font-size:0.82rem; text-transform:uppercase; margin-top:3px; }
|
|
body{padding:20px}
|
|
</style>
|
|
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
|
<script src="/static/shared/sidebar.js" defer data-active="audit"></script>
|
|
</head>
|
|
<body>
|
|
<h1>📜 Audit Log</h1>
|
|
<div class="sub">Kompletna povijest izmjena s blockchain pečatima na Polygon PoS</div>
|
|
|
|
<div class="stats" id="stats">
|
|
<div class="stat"><div class="v" id="s-total">—</div><div class="l">Ukupno akcija</div></div>
|
|
<div class="stat"><div class="v" id="s-today">—</div><div class="l">Danas</div></div>
|
|
<div class="stat"><div class="v" id="s-sealed">—</div><div class="l">Polygon zapečaćeno</div></div>
|
|
<div class="stat"><div class="v" id="s-users">—</div><div class="l">Aktivni korisnici</div></div>
|
|
</div>
|
|
|
|
<div class="toolbar">
|
|
<input id="f-q" placeholder="🔍 Pretraži..." />
|
|
<select id="f-action">
|
|
<option value="">Sve akcije</option>
|
|
<option value="create">CREATE</option>
|
|
<option value="update">UPDATE</option>
|
|
<option value="delete">DELETE</option>
|
|
<option value="seal">SEAL</option>
|
|
</select>
|
|
<select id="f-resource">
|
|
<option value="">Svi resursi</option>
|
|
<option value="users">Korisnici</option>
|
|
<option value="klubovi">Klubovi</option>
|
|
<option value="invoices">Računi</option>
|
|
<option value="putni_nalozi">Putni nalozi</option>
|
|
<option value="sufinanciranje">Sufinanciranje</option>
|
|
</select>
|
|
<button class="btn" onclick="load()">Filtriraj</button>
|
|
<button class="btn secondary" onclick="window.location.href='/app'">← Natrag na app</button>
|
|
</div>
|
|
|
|
<table id="tbl">
|
|
<thead>
|
|
<tr>
|
|
<th>Vrijeme</th>
|
|
<th>Korisnik</th>
|
|
<th>Akcija</th>
|
|
<th>Resurs</th>
|
|
<th>Detalji</th>
|
|
<th>Polygon Tx</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tbody">
|
|
<tr><td colspan="6" class="empty">⏳ Učitavam...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<script>
|
|
async function load() {
|
|
const q = document.getElementById('f-q').value;
|
|
const action = document.getElementById('f-action').value;
|
|
const resource = document.getElementById('f-resource').value;
|
|
const tbody = document.getElementById('tbody');
|
|
|
|
let url = '/sport/api/audit/log?limit=200';
|
|
if (q) url += '&q=' + encodeURIComponent(q);
|
|
if (action) url += '&action=' + action;
|
|
if (resource) url += '&resource=' + resource;
|
|
|
|
try {
|
|
const r = await fetch(url);
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
const data = await r.json();
|
|
const items = data.items || data.entries || data || [];
|
|
|
|
if (!items.length) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="empty">📭 Nema zapisa</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = items.map(item => {
|
|
const action = (item.action || 'unknown').toLowerCase();
|
|
const klasa = action.includes('seal') ? 'b-seal' :
|
|
action.includes('create') ? 'b-create' :
|
|
action.includes('update') ? 'b-update' :
|
|
action.includes('delete') ? 'b-delete' : 'b-update';
|
|
const tx = item.tx_hash || item.polygon_tx || '';
|
|
const txLink = tx ? `<a href="https://polygonscan.com/tx/${tx}" target="_blank" class="tx-link">${tx.substring(0,16)}...</a>` : '<span style="color:#5a6072">—</span>';
|
|
const ts = new Date(item.created_at || item.timestamp).toLocaleString('hr-HR');
|
|
const details = item.details || item.diff || item.message || '';
|
|
const detStr = typeof details === 'object' ? JSON.stringify(details).substring(0,80)+'...' : String(details).substring(0,80);
|
|
|
|
return `<tr>
|
|
<td>${ts}</td>
|
|
<td>${item.user_email || item.user_name || item.actor || '—'}</td>
|
|
<td><span class="badge ${klasa}">${(item.action || '').toUpperCase()}</span></td>
|
|
<td>${item.resource_type || item.resource || item.target || '—'}</td>
|
|
<td>${detStr}</td>
|
|
<td>${txLink}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
} catch (e) {
|
|
tbody.innerHTML = `<tr><td colspan="6" class="empty">⚠ Greška: ${e.message}</td></tr>`;
|
|
}
|
|
|
|
// Stats
|
|
try {
|
|
const sr = await fetch('/sport/api/audit/stats');
|
|
if (sr.ok) {
|
|
const s = await sr.json();
|
|
document.getElementById('s-total').textContent = s.total || '—';
|
|
document.getElementById('s-today').textContent = s.today || '—';
|
|
document.getElementById('s-sealed').textContent = s.sealed || '—';
|
|
document.getElementById('s-users').textContent = s.users || '—';
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
load();
|
|
setInterval(load, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|