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:
@@ -206,6 +206,31 @@ def me_gdpr_consent(user = Depends(require_user)):
|
||||
ORDER BY consent_at DESC LIMIT 50""", (user["id"],))
|
||||
return {"current": rows[0] if rows else None, "history": rows}
|
||||
|
||||
# ─────────────────────────── Article 7 — withdraw consent ───────────────────────────
|
||||
# GDPR Art. 7(3): "the data subject shall have the right to withdraw his or
|
||||
# her consent at any time. The withdrawal of consent shall be as easy as to
|
||||
# give consent."
|
||||
@me_router.post("/withdraw-consent")
|
||||
@me_router.delete("/gdpr-consent")
|
||||
def me_withdraw_consent(request: Request, user = Depends(require_user)):
|
||||
"""Withdraw all non-necessary consent (analytics + marketing).
|
||||
Records a fresh consent row with everything but `necessary` = false and
|
||||
clears users.gdpr_consent_at so the cookie banner shows again on next
|
||||
login. Necessary cookies (session, CSRF) remain — they're legitimate
|
||||
interest, not consent-based."""
|
||||
ip, ua = _client(request)
|
||||
db_exec("""INSERT INTO pgz_sport.gdpr_consent
|
||||
(user_id, session_id, ip, necessary, analytics, marketing, policy_version, user_agent)
|
||||
VALUES (%s, NULL, %s, true, false, false, %s, %s)""",
|
||||
(user["id"], ip, POLICY_VERSION, ua))
|
||||
db_exec("UPDATE pgz_sport.users SET gdpr_consent_at=NULL WHERE id=%s",
|
||||
(user["id"],))
|
||||
audit(user["id"], "gdpr.consent.withdraw",
|
||||
meta={"reason": "user_requested"}, ip=ip, ua=ua)
|
||||
return {"status": "ok",
|
||||
"message": "Pristanak za neobvezne kolačiće povučen. Nužni kolačići i dalje vrijede temeljem legitimnog interesa.",
|
||||
"policy_version": POLICY_VERSION}
|
||||
|
||||
# ─────────────────────────── Admin: erasure queue ───────────────────────────
|
||||
@admin_router.get("/erasure-requests")
|
||||
def list_erasure_requests(status: Optional[str] = None,
|
||||
|
||||
Reference in New Issue
Block a user