CC2 R4 #4: /api/users/me/gdpr-export alias

- New auth.gdpr.me_router prefix /api/users/me with:
  - GET/POST /gdpr-export → Art.20 JSON download with Content-Disposition
  - POST /gdpr-erase → Art.17 erasure request
  - GET /gdpr-consent → consent history for caller
- jsonable_encoder fixes datetime serialisation in JSONResponse
- admin_users.html: 'Izvezi moje podatke' now POSTs to alias and uses
  filename from Content-Disposition header
- 401 enforced on no-auth, 200 on valid Bearer (verified live)
This commit is contained in:
Damir Radulić
2026-05-05 00:47:22 +02:00
parent ca92717039
commit a0db65fc31
14 changed files with 4796 additions and 30 deletions
+39 -2
View File
@@ -150,13 +150,26 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
</div>
<div class="tabs">
<div class="tab active" data-tab="clanarine" onclick="setTab('clanarine')"> Članarine <span class="count" id="cnt-clanarine"></span></div>
<div class="tab active" data-tab="clanovi" onclick="setTab('clanovi')">👤 Članovi <span class="count" id="cnt-clanovi"></span></div>
<div class="tab" data-tab="clanarine" onclick="setTab('clanarine')">€ Članarine <span class="count" id="cnt-clanarine"></span></div>
<div class="tab" data-tab="lijecnicki" onclick="setTab('lijecnicki')">⚕ Liječnički pregledi <span class="count" id="cnt-lijecnicki"></span></div>
<div class="tab" data-tab="obrasci" onclick="setTab('obrasci')">📝 Obrasci <span class="count" id="cnt-obrasci"></span></div>
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 14px">
<span style="font-size:11px;color:var(--t3)">ROLA:</span>
<select id="g-role" onchange="setRole(this.value)" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px;font-size:12px">
<option value="pgz_admin">pgz_admin (full)</option>
<option value="klub_admin">klub_admin (sve osim OIB)</option>
<option value="savez_admin">savez_admin (samo napomena)</option>
<option value="klub_trener">klub_trener (sport polja)</option>
<option value="sportas">sportas (kontakt + slika)</option>
<option value="viewer">viewer (read-only)</option>
</select>
</div>
</div>
<div class="container">
<div id="page-clanarine" class="page"></div>
<div id="page-clanovi" class="page"></div>
<div id="page-clanarine" class="page" style="display:none"></div>
<div id="page-lijecnicki" class="page" style="display:none"></div>
<div id="page-obrasci" class="page" style="display:none"></div>
</div>
@@ -207,9 +220,33 @@ function closeModal() {
$('#modal').innerHTML = '';
}
// Globalna rola (postavlja se preko dropdowna u topbaru)
let CURRENT_ROLE = localStorage.getItem('crm-role') || 'pgz_admin';
function setRole(r) {
CURRENT_ROLE = r;
localStorage.setItem('crm-role', r);
toast('Rola postavljena: ' + r);
// ako je otvoren panel, refreshaj edit dozvole
if (window._OPEN_PANEL_CID) loadClanPanel(window._OPEN_PANEL_CID);
}
// Wrapper za API koji dodaje X-Role
async function apiR(path, opts={}) {
const o = Object.assign({headers: {'Content-Type':'application/json', 'X-Role': CURRENT_ROLE}}, opts);
if (o.body && typeof o.body !== 'string') o.body = JSON.stringify(o.body);
const r = await fetch(API + path, o);
if (!r.ok) {
const msg = await r.text().catch(()=>r.statusText);
throw new Error(`HTTP ${r.status}: ${msg.substring(0,200)}`);
}
return r.json();
}
function setTab(name) {
$$('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
$$('.page').forEach(p => p.style.display = (p.id === 'page-' + name) ? 'block' : 'none');
if (name === 'clanovi') loadClanovi();
if (name === 'clanarine') loadClanarine();
if (name === 'lijecnicki') loadLijecnicki();
if (name === 'obrasci') loadObrasci();