R7: GDPR /users/me/request-deletion alias + remove duplicate profileDeleteAccount

- 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
This commit is contained in:
2026-05-05 02:06:34 +02:00
parent 28fa98d83f
commit 67372d6c58
15 changed files with 2368 additions and 63 deletions
+85 -11
View File
@@ -257,8 +257,8 @@ table tbody tr:hover{background:var(--bg3)}
.role-switch{display:none}
}
</style>
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
<script src="/sport/static/shared/sidebar.js" defer data-active="profil"></script>
<link rel="stylesheet" href="/static/shared/sidebar.css">
<script src="/static/shared/sidebar.js" defer data-active="profil"></script>
</head>
<body>
@@ -332,7 +332,15 @@ async function api(path){
}
// JWT-aware fetch wrapper
function getToken(){ try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; } catch(e){ return ''; } }
function getToken(){
try {
return localStorage.getItem('pgz_access')
|| sessionStorage.getItem('pgz_access')
|| localStorage.getItem('jwt')
|| localStorage.getItem('access_token')
|| '';
} catch(e){ return ''; }
}
async function apiAuth(path, opts){
opts = opts || {};
const h = Object.assign({}, opts.headers || {});
@@ -631,7 +639,7 @@ function logout(){
localStorage.removeItem('jwt');
} catch(e){}
alert('Odjavljen. (Production: redirect na /login)');
window.location.href = '/sport/static/sport2.html';
window.location.href = '/static/sport2.html';
}
//=========== SECTION TITLES ===========
@@ -817,8 +825,8 @@ function profileRender(){
Imaš pravo na pristup, izmjenu i brisanje svojih osobnih podataka prema GDPR uredbi (čl. 1517, 20).
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" onclick="alert('Izvoz JSON svih podataka — backend M10')">📤 Izvezi moje podatke (JSON)</button>
<button class="btn" onclick="alert('Pregled audit zapisa o pristupu — M10')">🔍 Audit pristupa mojim podacima</button>
<button class="btn" onclick="gdprExport()">📤 Izvezi moje podatke (JSON)</button>
<button class="btn" onclick="gdprAuditMy()">🔍 Audit pristupa mojim podacima</button>
<button class="btn" style="border-color:var(--red);color:var(--red)" onclick="profileDeleteAccount()">🗑 Zatraži brisanje računa</button>
</div>
</div>
@@ -832,7 +840,7 @@ SECTIONS['sportas:profil']= profileRender;
// Profile actions
function pickAvatar(){
if(!getToken()){
alert('Avatar upload zahtijeva login (JWT). U demo modu nije dostupan.');
alert('Niste prijavljeni. Idite na /login pa se prijavite kao damir@pgz.hr / PGZ2026!');
return;
}
$('#avatar-input').click();
@@ -933,10 +941,7 @@ async function profileVerify2FA(){
if(r && r.status==='ok'){ alert('2FA aktivirano ✓'); closeDetail(); loadSection(); }
else alert('Pogrešan kod.');
}
function profileDeleteAccount(){
if(!confirm('Zaista zatraži brisanje računa? GDPR brisanje je nepovratno.')) return;
alert('Zahtjev za brisanje poslan na PGŽ admin (M10 — backend).');
}
// profileDeleteAccount: real implementation below (line ~1902)
// =======================================================================
// PGŽ ADMIN — Dashboard
@@ -1839,6 +1844,75 @@ async function init(){
navTo('profil');
}
window.addEventListener('DOMContentLoaded', init);
//=========== GDPR ===========
async function gdprExport() {
const tok = getToken();
if (!tok) { alert('Niste prijavljeni. Idite na /login'); return; }
try {
const r = await fetch(API + '/users/me/gdpr-export', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + tok }
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
// Download as JSON file
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'gdpr-export-' + (data.user?.email || 'me') + '-' + new Date().toISOString().slice(0,10) + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('✓ Izvoz uspješan! Datoteka spremljena.');
} catch (e) {
alert('Greška pri izvozu: ' + e.message);
}
}
async function gdprAuditMy() {
const tok = getToken();
if (!tok) { alert('Niste prijavljeni'); return; }
try {
const r = await fetch(API + '/audit/log?user_id=me&limit=100', {
headers: { 'Authorization': 'Bearer ' + tok }
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const items = data.items || data.entries || [];
if (!items.length) {
alert('Nema audit zapisa za vaš račun.');
return;
}
const txt = items.slice(0, 30).map(e => {
const ts = new Date(e.created_at || e.timestamp).toLocaleString('hr-HR');
return ts + ' • ' + (e.action || '?') + ' • ' + (e.resource_type || '?') + ' • ' + (e.user_email || '?');
}).join('\n');
alert('Audit zapisi (zadnjih ' + Math.min(items.length, 30) + '):\n\n' + txt);
} catch (e) {
alert('Greška: ' + e.message);
}
}
async function profileDeleteAccount() {
if (!confirm('Sigurno želite zatražiti BRISANJE računa? Ovo je trajno.')) return;
const reason = prompt('Razlog brisanja (opcionalno):', '');
const tok = getToken();
if (!tok) { alert('Niste prijavljeni'); return; }
try {
const r = await fetch(API + '/users/me/request-deletion', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok},
body: JSON.stringify({reason: reason || ''})
});
if (r.ok) alert('✓ Zahtjev poslan. Bit ćete kontaktirani u 30 dana.');
else alert('Greška: HTTP ' + r.status);
} catch (e) {
alert('Greška: ' + e.message);
}
}
</script>
</body>
</html>