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:
@@ -230,6 +230,7 @@ table.dt tr:hover td{background:rgba(0,48,135,.1);cursor:pointer}
|
||||
.sh-stats{grid-template-columns:repeat(3,1fr)}
|
||||
}
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -711,7 +712,7 @@ async function openSavezDetail(id){
|
||||
<div class="srow"><label>Sport</label><span>${d.sport||'–'}</span></div>
|
||||
<div class="srow"><label>Predsjednik</label><span style="color:var(--cyan)">${d.predsjednik||'–'}</span></div>
|
||||
<div class="srow"><label>Tajnik</label><span style="color:${d.tajnik?'var(--t1)':'var(--red)'}">${d.tajnik||'NULL'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${d.oib||'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${d.oib?formatOib(d.oib,{savez_id:d.id}):'–'}</span></div>
|
||||
<div class="srow"><label>Email</label><span>${d.email||'–'}</span></div>
|
||||
</div>
|
||||
<div class="tabs" id="sv-tabs">
|
||||
@@ -815,7 +816,7 @@ async function openKlubDetail(id){
|
||||
<div class="srow"><label>Predsjednik</label><span style="color:var(--cyan)">${k.predsjednik||'–'}</span></div>
|
||||
<div class="srow"><label>Tajnik</label><span style="color:${k.tajnik?'var(--t1)':'var(--red)'}">${k.tajnik||'NULL'}</span></div>
|
||||
<div class="srow"><label>Savez</label><span>${k.savez_naziv||k.savez||'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${k.oib||'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'–'}</span></div>
|
||||
<div class="srow"><label>Sjedište</label><span style="font-size:10px">${k.sjediste||'–'}</span></div>
|
||||
<div class="srow"><label>Razina</label><span>${k.razina||'–'}</span></div>
|
||||
</div>
|
||||
@@ -909,7 +910,7 @@ async function openSportasProfil(id){
|
||||
</div>
|
||||
<div class="tab-c on" id="sp-t-bio">
|
||||
<div style="background:var(--bg3);border-radius:var(--r);padding:10px">
|
||||
<div class="srow"><label>OIB</label><span class="mn">${c.oib?'••'+c.oib.slice(-3):'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${c.oib?formatOib(c.oib,{klub_id:c.klub_id,savez_id:c.savez_id}):'–'}</span></div>
|
||||
<div class="srow"><label>Datum rodjenja</label><span>${c.datum_rodenja||c.datum_rodjenja||'–'}</span></div>
|
||||
<div class="srow"><label>Spol</label><span>${c.spol||'–'}</span></div>
|
||||
<div class="srow"><label>Visina/Težina</label><span>${c.visina_cm||'–'} cm / ${c.tezina_kg||'–'} kg</span></div>
|
||||
@@ -1025,7 +1026,7 @@ async function loadClanarine(){
|
||||
return `<tr onclick="openSportasProfil(${r.clan_id})" style="cursor:pointer">
|
||||
<td style="display:flex;align-items:center;gap:7px">
|
||||
<div style="width:28px;height:28px;border-radius:4px;background:var(--bg3);overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:12px">${r.slika_url?`<img src="${r.slika_url}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:r.spol==='Ž'?'👩':'👤'}</div>
|
||||
<div><div style="font-weight:600">${r.ime||''} ${r.prezime||''}</div><div style="font-size:8px;color:var(--t4);font-family:var(--mono)">${r.oib||'–'}</div></div>
|
||||
<div><div style="font-weight:600">${r.ime||''} ${r.prezime||''}</div><div style="font-size:8px;color:var(--t4);font-family:var(--mono)">${r.oib?formatOib(r.oib,{klub_id:r.klub_id,savez_id:r.savez_id}):'–'}</div></div>
|
||||
</td>
|
||||
<td style="font-size:10px;color:var(--t2)">${['','I','II','III','IV','V','VI'][r.hoo_kategorija||0]||'–'}</td>
|
||||
<td class="mn">${r.godina||'–'}</td>
|
||||
|
||||
Reference in New Issue
Block a user