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)
This commit is contained in:
2026-05-05 09:14:46 +02:00
parent 31e0374465
commit 8e136351f9
27 changed files with 2323 additions and 56 deletions
+7 -2
View File
@@ -138,6 +138,7 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
<link rel="stylesheet" href="/static/shared/sidebar.css">
<script src="/static/shared/sidebar.js" defer data-active="clanarine"></script>
<style>body{padding-top:0}</style>
<script src="/static/oib_format.js" defer></script>
</head>
<body>
@@ -1235,7 +1236,11 @@ async function loadClanPanel(cid) {
// helper za render polja s edit/no-edit
const f = (key, label, val, type='text') => {
const ed = canEdit(key);
const safe = val == null || val === '' ? '—' : String(val);
let safe = val == null || val === '' ? '—' : String(val);
// role-based PII rendering for OIB
if (key === 'oib' && safe !== '—') {
safe = formatOib(safe, {klub_id: c.klub_id, savez_id: c.savez_id});
}
return `
<div class="payment-row">
<div class="l">${esc(label)}${ed?'':' <span style="color:var(--t3);font-size:9px">🔒</span>'}</div>
@@ -1313,7 +1318,7 @@ async function loadClanPanel(cid) {
<div class="payment-card">
<div class="payment-row"><div class="l">Trenutni klub</div><div class="v">${esc(k.naziv || '—')}</div></div>
${k.savez_naziv ? `<div class="payment-row"><div class="l">Savez</div><div class="v">${esc(k.savez_naziv)}</div></div>` : ''}
${k.oib ? `<div class="payment-row"><div class="l">OIB kluba</div><div class="v">${esc(k.oib)}</div></div>` : ''}
${k.oib ? `<div class="payment-row"><div class="l">OIB kluba</div><div class="v">${esc(formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}))}</div></div>` : ''}
${k.iban ? `<div class="payment-row"><div class="l">IBAN</div><div class="v">${esc(k.iban)}</div></div>` : ''}
</div>
${d.povijest_klubova && d.povijest_klubova.length ? `