#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.
- enrich_apply now imports audit_seal_router.audit_log and writes a sys_audit
row after every successful UPDATE: action='enrich.apply', target_type=kind,
target_id=eid, payload={applied: {...}, sources: [...]}, user from headers.
- Other modules (cc2 users, cc4 invoices/putni_nalozi, cc5 clanarine/lijecnicki/
obrasci) can call the same helper:
from audit_seal_router import audit_log
audit_log(action='users.update', target_type='users', target_id=u['id'],
payload={'changed':[...]}, user_email=actor)
- Verified: real apply on klub 4528 produced sys_audit id 102.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
- erp/putni_nalozi.py: FastAPI router /api/erp/putni-nalog
- GET /preview: live obračun dnevnica + kilometrine za UI
- POST /putni-nalog: kreiraj (draft) iz UI forme (voditelj, putnici, od→do, km)
- PUT /putni-nalog/{id}: izmjena s recompute dnevnica
- POST /putni-nalog/{id}/odobriti: status=odobren
- POST /putni-nalog/{id}/zatvori: linkanje računa (invoice_ids), končan obračun
- HR 2025: domaće 30 € (>8h), 15 € (5–8h), 0 € (<5h); inozemne po zemlji (NN tablica)
- km × 0.50 €/km (neoporezivi limit 2025)
- Live test: Rijeka→Zagreb 3 dana = 3 dnevnice × 30 € + 380 km × 0.50 € = 280 € prije računa, 455 € sa hotelom+meals
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>