CC2 R4 #2+#5: remove legacy unauth /api/admin/users — close 401 gap
The bare @app.get/post('/api/admin/users') decorators in pgz_sport_api.py
were registered before app.include_router(admin_users_router) and shadowed
the JWT-protected M2 routes, leaking user list to anyone.
Removed all three: GET /api/admin/users, POST /api/admin/users,
POST /api/admin/users/{uid}/toggle. The auth.admin_users router now owns
this prefix exclusively and gates every method with require_user.
Verified: no-auth → 401, invalid token → 401, valid Bearer → 200.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<!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; }
|
||||
</style>
|
||||
</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>
|
||||
Reference in New Issue
Block a user