Files
damir 8e136351f9 CRISIS FIX: login flow + mobile responsive + token expiry handling
ROOT CAUSE ISOLATED:
Backend POST /api/auth/login, GET/PUT /api/auth/me, POST avatar, POST /logout
all return 200 OK (verified curl). Damirov problem is browser-side:
stale localStorage tokens that don't match current backend → 401 cascade
→ avatar upload appears as 'failed: 401' → profile changes 'lost'.

FIXES:
1. apiAuth() in app.html now:
   - Pre-checks JWT exp claim before request
   - On 401 response: clears localStorage (pgz_access/refresh/user) +
     redirects to /login?reason=unauthorized
   - On JWT expired: redirects to /login?reason=expired

2. login.html displays toast for ?reason=expired/unauthorized

3. Mobile responsive CSS (max-width: 768px):
   - app.html: hamburger menu, sidebar slide-in, full-width drill-down panel
   - sport2.html: KPI grid 2-col, klubovi 1-col, tables horizontal scroll
   - Both: viewport meta + media queries + touch-friendly buttons

4. Mobile menu toggle button + backdrop overlay added

VERIFIED E2E (curl):
- POST /auth/login → 200 + JWT
- GET /auth/me → 200 + telefon persisted
- PUT /auth/me → 200, DB row updated
- POST /auth/me/avatar → 200, file saved + avatar_url returned
- POST /auth/logout → 200, token revoked (next /me returns 401)
2026-05-05 09:14:46 +02:00

170 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PGŽ Sport · Politika privatnosti</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%2306080d'/><text x='16' y='23' text-anchor='middle' font-size='18' font-family='monospace' fill='%2300f0ff'>P</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #06080d;
--bg-2: #0d1117;
--bg-3: #161b22;
--border: #1f2937;
--text: #e6edf3;
--text-2: #8b949e;
--text-3: #6e7681;
--accent: #00f0ff;
--accent-2: #00b8d4;
--green: #56d364;
--yellow: #d29922;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { font-family: 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; font-size: 14px; line-height: 1.65; }
.wrap { max-width: 880px; margin: 0 auto; padding: 56px 28px 96px; }
header { border-bottom: 1px solid var(--border); padding-bottom: 24px; margin-bottom: 32px; }
h1 { font-size: 28px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 6px; }
.kicker { color: var(--accent); font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
.meta { color: var(--text-2); font-size: 12px; font-family: 'JetBrains Mono', monospace; }
h2 { font-size: 18px; font-weight: 600; margin: 36px 0 12px; color: var(--text); border-left: 3px solid var(--accent); padding-left: 12px; }
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 8px; color: var(--text); }
p, li { color: var(--text-2); margin-bottom: 10px; }
strong { color: var(--text); font-weight: 600; }
ul { padding-left: 22px; margin-bottom: 12px; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.box { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; padding: 18px 22px; margin: 16px 0; }
.box.warn { border-color: var(--yellow); }
.box.ok { border-color: var(--green); }
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; font-size: 13px; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); }
th { color: var(--accent); font-weight: 600; font-size: 11px; letter-spacing: 1px; text-transform: uppercase; }
td { color: var(--text-2); }
td strong { color: var(--text); }
code { font-family: 'JetBrains Mono', monospace; font-size: 12px; background: var(--bg-3); padding: 1px 6px; border-radius: 3px; color: var(--accent); }
.footer-back { margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; font-size: 12px; color: var(--text-3); }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">PGŽ Sport ERP/CRM · v1 · 2026</div>
<h1>Politika privatnosti i zaštite osobnih podataka</h1>
<div class="meta">Verzija dokumenta: <strong>v1</strong> · Stupa na snagu: 2026-05-04 · Posljednja izmjena: 2026-05-05</div>
</header>
<div class="box">
<p><strong>Voditelj obrade:</strong> Primorsko-goranska županija — Odjel za sport, Slogin kula 2/IV, Rijeka</p>
<p><strong>Kontakt za GDPR:</strong> <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a></p>
<p><strong>Službenik za zaštitu podataka (DPO):</strong> Damir Radulić — <a href="mailto:damir@rinet.one">damir@rinet.one</a></p>
</div>
<h2>1. Koje podatke prikupljamo</h2>
<p>Platforma PGŽ Sport prikuplja i obrađuje sljedeće kategorije osobnih podataka, sukladno Općoj uredbi o zaštiti podataka (GDPR — Uredba (EU) 2016/679) i Zakonu o provedbi Opće uredbe o zaštiti podataka (NN 42/18):</p>
<ul>
<li><strong>Identifikacijski podaci:</strong> ime, prezime, OIB, datum rođenja, spol</li>
<li><strong>Kontakt podaci:</strong> e-pošta, broj telefona, adresa kluba/saveza</li>
<li><strong>Funkcijski podaci:</strong> uloga (predsjednik, tajnik, član, trener), klub/savez, kategorija</li>
<li><strong>Tehnički podaci:</strong> IP adresa prilikom prijave, identifikator sesije, podaci o uređaju (User-Agent), vrijeme prijave</li>
<li><strong>Sigurnosni podaci:</strong> lozinka (hash), 2FA tajna (kriptirana), revocirani tokeni</li>
<li><strong>Sportski podaci:</strong> licence, kategorizacija, liječnički pregledi, članarine, transferi</li>
</ul>
<h2>2. Pravna osnova obrade (čl. 6 GDPR)</h2>
<table>
<thead><tr><th>Kategorija obrade</th><th>Pravna osnova</th><th>Članak</th></tr></thead>
<tbody>
<tr><td><strong>Prijava, sigurnost sesije, audit log</strong></td><td>Legitimni interes voditelja obrade</td><td>čl. 6(1)(f)</td></tr>
<tr><td><strong>Vođenje registra sportskih klubova</strong></td><td>Pravna obveza (Zakon o sportu, NN 141/22)</td><td>čl. 6(1)(c)</td></tr>
<tr><td><strong>Obrada zahtjeva za sufinanciranje</strong></td><td>Izvršavanje zadaće u javnom interesu</td><td>čl. 6(1)(e)</td></tr>
<tr><td><strong>Analitički kolačići</strong></td><td>Privola (opt-in)</td><td>čl. 6(1)(a)</td></tr>
<tr><td><strong>Marketinške komunikacije</strong></td><td>Privola (opt-in)</td><td>čl. 6(1)(a)</td></tr>
</tbody>
</table>
<h2>3. Vaša prava (čl. 1522 GDPR)</h2>
<h3>Članak 15 — Pravo na pristup</h3>
<p>Imate pravo dobiti potvrdu obrađuju li se Vaši osobni podaci te pristup tim podacima. Implementirano kroz: <code>GET /api/users/me/gdpr-export</code> (vraća JSON s kompletnim profilom, sesijama, audit logom, povijesti privola, vezama na klub/savez).</p>
<h3>Članak 16 — Pravo na ispravak</h3>
<p>Imate pravo zatražiti ispravak netočnih podataka. Implementirano kroz: <code>PUT /api/auth/me</code> (ime, prezime, OIB, telefon, jezik) i sučelje "Moj profil".</p>
<h3>Članak 17 — Pravo na brisanje ("pravo na zaborav")</h3>
<p>Imate pravo zatražiti brisanje Vaših osobnih podataka kada osnova za obradu prestane. Implementirano kroz: <code>POST /api/users/me/gdpr-erase</code> ili <code>POST /api/gdpr/erase</code>. Zahtjev se obrađuje u roku od 30 dana. Nakon odobrenja, identifikacijski podaci se anonimiziraju (e-pošta postaje <code>erased-{id}@anonymous.gdpr</code>, ime postaje "Erased", OIB i telefon se brišu).</p>
<div class="box warn">
<p><strong>Napomena:</strong> Pojedini podaci moraju ostati zbog pravne obveze (npr. revizijski trag financijskih transakcija — Zakon o računovodstvu, 11 godina). U tom slučaju podaci se pseudonimiziraju, ali ne brišu u potpunosti.</p>
</div>
<h3>Članak 18 — Pravo na ograničenje obrade</h3>
<p>Imate pravo zatražiti privremeno ograničenje obrade dok se ne riješi spor o točnosti podataka. Kontaktirajte <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a>.</p>
<h3>Članak 20 — Pravo na prenosivost podataka</h3>
<p>Imate pravo dobiti svoje podatke u strukturiranom, uobičajeno korištenom i strojno čitljivom formatu (JSON). Implementirano kroz: <code>GET /api/users/me/gdpr-export</code>.</p>
<h3>Članak 21 — Pravo na prigovor</h3>
<p>Imate pravo prigovoriti obradi temeljenoj na legitimnom interesu. Kontaktirajte <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a>.</p>
<h3>Članak 7(3) — Povlačenje privole</h3>
<p>Privola za neobvezne kolačiće (analitika, marketing) može se povući u bilo kojem trenutku, jednako jednostavno kao što je dana. Implementirano kroz: <code>POST /api/users/me/withdraw-consent</code> ili <code>DELETE /api/users/me/gdpr-consent</code>.</p>
<h2>4. Kolačići</h2>
<table>
<thead><tr><th>Tip</th><th>Svrha</th><th>Trajanje</th><th>Pravna osnova</th></tr></thead>
<tbody>
<tr><td><strong>Nužni</strong></td><td>Sesija, CSRF, sigurnost prijave</td><td>Sesija</td><td>Legitimni interes</td></tr>
<tr><td><strong>Funkcionalni</strong></td><td>Postavke jezika, tema, sidebar stanje</td><td>30 dana</td><td>Privola</td></tr>
<tr><td><strong>Analitički</strong></td><td>Anonimne statistike korištenja</td><td>365 dana</td><td>Privola</td></tr>
<tr><td><strong>Marketinški</strong></td><td>Trenutno se ne koriste</td><td></td><td>Privola</td></tr>
</tbody>
</table>
<h2>5. Razdoblja čuvanja</h2>
<ul>
<li><strong>Audit log</strong> (prijave, izmjene): 5 godina</li>
<li><strong>Sesijski tokeni:</strong> max 90 dana, a po odjavi se opozivaju</li>
<li><strong>Korisnički profili:</strong> dok je račun aktivan + 1 godina nakon deaktivacije</li>
<li><strong>Financijski podaci:</strong> 11 godina (Zakon o računovodstvu, čl. 8)</li>
<li><strong>Podaci o članovima klubova:</strong> dok je član registriran u klubu + 5 godina</li>
</ul>
<h2>6. Sigurnosne mjere</h2>
<ul>
<li>HTTPS (TLS 1.3) za sav promet</li>
<li>Lozinke pohranjene kao Argon2/bcrypt hash</li>
<li>Dvofaktorska autentikacija (TOTP) dostupna svim korisnicima</li>
<li>Audit log svih akcija sa IP adresom i User-Agentom</li>
<li>OIB se prikazuje samo administratorima; za ostale korisnike se maskira (<code>•••XXX••</code>)</li>
<li>Pristup po načelu najmanjih ovlasti (RBAC) — uloge: super_admin, pgz_admin, savez_admin, klub_admin, klub_user, klub_clan, viewer</li>
</ul>
<h2>7. Dijeljenje podataka s trećim stranama</h2>
<p>Vaši podaci se <strong>ne prodaju</strong> i <strong>ne ustupaju</strong> trećim stranama u marketinške svrhe. Podaci se mogu razmjenjivati isključivo s:</p>
<ul>
<li>Hrvatskim sportskim savezom — kada je to pravna obveza za registraciju kluba/člana</li>
<li>Ministarstvom turizma i sporta — pri prijavi za sufinanciranje</li>
<li>Nadležnim tijelima (sud, policija) — na temelju pravomoćnog naloga</li>
</ul>
<h2>8. Pritužbe</h2>
<p>Pritužbu na obradu osobnih podataka možete podnijeti:</p>
<ul>
<li>Voditelju obrade: <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a></li>
<li>Službeniku za zaštitu podataka: <a href="mailto:damir@rinet.one">damir@rinet.one</a></li>
<li>Agenciji za zaštitu osobnih podataka (AZOP), Selska cesta 136, Zagreb — <a href="https://azop.hr" target="_blank" rel="noopener">azop.hr</a></li>
</ul>
<div class="box ok">
<p><strong>Strojno čitljiva verzija ove politike:</strong> dostupna na <code>GET /api/gdpr/policy</code> u JSON formatu (verzija, URL, popis prava, kontakti).</p>
</div>
<div class="footer-back">
<span>© 2026 Primorsko-goranska županija · Odjel za sport</span>
<span><a href="/sport/static/login.html">← Povratak na prijavu</a></span>
</div>
</div>
</body>
</html>