1) HNS direct link u research_links: za sportaš s profile_url/source_url
(npr. https://semafor.hns.family/igraci/X/...) generira [⭐DIRECT] link na vrhu liste,
umjesto generic Google search. _research_links sada prima row dict.
2) Avatar cache buster: applyMeToHeader dodaje ?t=Date.now() na sve avatar img tagove.
Avatar upload handler dodatno persistira novi avatar_url u localStorage.pgz_user
tako da preživi page refresh + cross-page navigacije.
3) Logo home link: <div class='logo'> → <a href='/' class='logo'> u app.html i sport2.html.
Klik na PGŽ SPORT logo vodi na public portal.
4) Klub → Sportaši drill-down: u klub Info tabu dodan button
'👥 Vidi sportaše ovog kluba (N)' koji prebacuje na k-clan tab.
Plus '🌐 Službena stranica' link kad klub ima web.
5) Smarter klub enrichment:
- URL validacija (skip placeholder strings poput 'godisnjak_zspgz_2025')
- Domain candidate guesser (slug → 16 candidate URLs s common HR TLD-ovima i sport prefix-ima)
- Parallel HEAD probe (8 threads, 10s budget) — first 200 + name token match wins
- Subpage scrape (/kontakt, /uprava, /o-nama, /o-klubu, /predsjednik) za richer evidence
- HNK Orijent (id 3766) test: pogađa https://www.orijent.hr/, predlaže web+email+telefon+opis
E2E verified:
- 9/9 sidebar URL-ova → 200
- /users/me/gdpr-export → 200 (28KB JSON)
- /users/me/request-deletion → 200 (DB row pgz_sport.gdpr_erasure_requests)
- /enrich/klub/3766 → 4 proposed fields (web, email, telefon, opis)
- HNS sportaš research_links: ⭐ HNS profil DIRECT link na vrhu
Backend: routers/enrich_router.py
Frontend: static/app.html, static/sport2.html
Backups: _backups/sprint_1777940670/
Tag: R7-demo-ready
- 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
#1 JWT middleware extended:
- Was: /api/admin/* only
- Now: any POST/PUT/PATCH/DELETE under /api/* requires Bearer JWT
- Whitelist (still anonymous): /api/auth/login, /refresh, /forgot-password,
/password/reset, /reset-password, /setup-password, /google;
/api/gdpr/consent; any path ending /avatar
- 14 mutating endpoints verified to return 401 without token
#2 Avatar upload demo mode (routers/clan_panel_router.py):
- Anonymous → returns {demo_mode:true, slika_url:null,
message:'Demo mode — slika nije spremljena. Prijavite se za pravu pohranu.'},
no FS write, no DB write
- Authenticated (valid JWT, allowed role) → real save as before
- Auth check now uses auth.auth_v2.decode_token (proper secret + revocation)
instead of the broken local _resolve_role
#3 Mock mailer (auth/mailer.py):
- send_email writes RFC 822 .eml to /tmp/pgz_mailbox + appends to INDEX.jsonl
- send_password_reset, send_invite helpers with HR text + HTML alt
- Real SMTP active when PGZ_SMTP_HOST is set (env-driven, off by default)
- forgot-password and admin invite both call mailer; audit logs mail status
#5 Rate limiting on /api/auth/login:
- Per-user: 5 wrong attempts → 5-minute DB-backed lockout
(was 5 → 15 min). Configurable via PGZ_LOGIN_LOCK_THRESHOLD/MINUTES.
- Per-IP: 10 fails / 5-min sliding window in-memory → HTTP 429
Configurable via PGZ_LOGIN_IP_THRESHOLD/WINDOW_SEC. Successful
login clears the IP counter.
- Failed attempts respond '(N/5) — račun je zaključan na 5 minuta'
- New audit actions: login.ratelimit.ip; login.fail meta now
includes fails count, locked, lock_minutes
#4 Live test report: 46/46 across 6 demo users — login, JWT gate on 14
mutating endpoints, public path whitelist, demo-mode avatar +
real save, forgot-password e-mail to mailbox, no-leak unknown email,
5-fail lockout, 423 during lockout, audit coverage.
- New auth.gdpr.me_router prefix /api/users/me with:
- GET/POST /gdpr-export → Art.20 JSON download with Content-Disposition
- POST /gdpr-erase → Art.17 erasure request
- GET /gdpr-consent → consent history for caller
- jsonable_encoder fixes datetime serialisation in JSONResponse
- admin_users.html: 'Izvezi moje podatke' now POSTs to alias and uses
filename from Content-Disposition header
- 401 enforced on no-auth, 200 on valid Bearer (verified live)
The bare @app.get/post('/api/admin/users') decorators in pgz_sport_api.py
were registered before app.include_router(admin_users_router) and shadowed
the JWT-protected M2 routes, leaking user list to anyone.
Removed all three: GET /api/admin/users, POST /api/admin/users,
POST /api/admin/users/{uid}/toggle. The auth.admin_users router now owns
this prefix exclusively and gates every method with require_user.
Verified: no-auth → 401, invalid token → 401, valid Bearer → 200.