Damir Radulic
2e022a7dcc
fix(URGENT): SPA fallback serves sport2.html + 9 routers __future__ position
...
BUGS FIXED:
1. _serve_spa_fallback() returned index.html instead of sport2.html
→ User clicked /analitika /sufinanciranje etc and got wrong UI (DABI title)
→ Should serve sport2.html (PGZ SPORT - Platforma) with Analiza/Mreza/Link tabs
2. 9 router files had "from __future__" NOT at top of file
→ SyntaxError on import → routers SKIPPED → intermittent API failures
→ Affected: ocr.py, ocr_router.py, putni_nalozi.py, obrasci_router.py,
clan_panel_router.py, audit_seal_router.py, erp_full_router.py,
notif_router.py, seal.py
ROOT CAUSE:
Prior dehardcode batch (Master Zakon #1 sweep) inserted env-loading
imports BEFORE "from __future__ import annotations" — Python parser
requires __future__ FIRST.
FIX:
- _serve_spa_fallback() candidates list: sport2.html first
- Moved __future__ to top (preserving shebang + encoding + comments) in all 9
VERIFIED:
- 0 failed routers (was 7+)
- Analiza API: 10/10 success ~60-87ms
- Summary API: 5/5 success ~40ms
- sport.rinet.one/ → PGZ SPORT - Platforma (Analiza+Mreza tabs)
- All 9 SPA fallback routes serve sport2.html
Damir uploaded screenshot showing Analiza tab working (2,049 igraca,
82 klubova) but described as intermittent — root cause was router fails
causing some API endpoints to be missing/unreliable. Fixed.
2026-05-18 15:45:22 +02:00
Damir Radulic
aca5051418
feat: /api/v2/analiza/* endpoints - sport analytics backend
2026-05-16 00:28:12 +02:00
CC4
8c97a5b778
CC4 R7 ERP S2: DELETE invoice + /putni-nalozi alias + /placanja + /export/putni.xlsx
...
erp/ocr.py:
- DELETE /api/erp/invoices/{id} (samo pgz_admin) + cascade payment cleanup + audit
(briše vezana payments, otkapča invoice_uploads.invoice_id NULL, audit log "delete")
erp/putni_nalozi.py:
- GET/POST /api/erp/putni-nalozi (alias plural od /putni-nalog) za CC1 brief kompatibilnost
- GET /api/erp/putni-nalozi/{id}
- PATCH /api/erp/putni-nalozi/{id} sa body.action: approve|reject|submit|pay (route kroz lifecycle)
- PATCH /api/erp/putni-nalog/{id} (singular alias)
- GET /api/erp/export/putni.xlsx — openpyxl 19 stupaca (klub, voditelj, ruta, datumi, km, dnevnice, ukupno, status...)
- GET /api/erp/placanja — lista neplaćenih računa + odobrenih putnih naloga (kandidati za isplatu)
- POST /api/erp/placanja {kind:invoice|putni_nalog, id, iban, model, opis, poziv_na_broj}
→ generira HUB-3 PDF + EPC QR (reuse crm.payments.build_hub3_pdf), pohranjuje u
_data/uploads/placanja/{kind}_{id}_HUB3_*.pdf
- GET /api/erp/placanja/{kind}/{id}/pdf → streama zadnji generirani PDF, ili kreira on-demand
- Dodan from pathlib import Path (fix NameError)
Live tests:
- DELETE /invoices/4 → 200 (test invoice obrisan)
- GET /putni-nalozi → 200, /putni-nalozi/1 → 200
- GET /placanja → 200 lista; POST → ok pdf 11 KB; GET pdf → 200 application/pdf %PDF-
- /placanja invoice 1 (INA €63.15) i putni_nalog 2 (€133.08) PDF generirani
- /export/putni.xlsx → 200 application/vnd.openxmlformats... PK header valid
- OCR INA gorivo: vendor=INA, OIB=27759560625, brutto=€63.15, PDV=€12.63, cat=gorivo
- UI 3× "Ri.NET AI" / 0× "DeepSeek"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-05 08:01:49 +02:00
Damir Radulić
f9ebcddf28
CC2 R6: middleware-wide JWT, avatar demo mode, mock mailer, login rate limit
...
#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.
2026-05-05 01:42:53 +02:00
CC4-PGZ-Sport
6752ecaf07
R5 ERP: bulk ops + XLSX export + HUB-3 PDF + stats + m2m + UI
...
Backend:
- pgz_sport.putni_nalog_racuni (m2m) — backfill iz attachments.invoice_ids
- erp/putni_nalozi.py:
* GET /putni-nalog/{id} sada vraća invoices (m2m) + suggested_invoices (auto-suggest po
klubu/datumu, ne-vezani)
* POST /putni-nalog/{id}/attach-invoice {invoice_id, kategorija}
* DELETE /putni-nalog/{id}/invoice/{invoice_id}
* GET /putni-nalog/{id}/hub3.pdf — A4 HUB-3 uplatnica + EPC QR (reuse crm.payments.build_hub3_pdf)
- erp/ocr.py:
* POST /invoices/bulk-pay {ids:[], paid_date, payment_method, iban_*, reference, tx_id}
* POST /invoices/bulk-cancel {ids:[], razlog} (audit per record)
* GET /export/invoices.xlsx — openpyxl, 17 stupaca (datum, izdavatelj, OIB, klub,
neto/PDV/brutto, status, IBAN, opis, kategorija); permission filtered
* GET /stats — month/quarter/year totals, by_kind breakdown, top_klubovi, putni_nalozi totals
UI (static/erp.html):
- Novi tab "📊 Statistika" (default) — 3 KPI kartice (mjesec/kvartal/godina) za račune
+ putne naloge, top klubovi godina, klub filter, Export XLSX gumb
- Računi tab: bulk toolbar (checkbox per row + Select All) → Plati sve modal
(IBAN platitelja, datum, ref) / Otkaži označene (prompt razlog) / Export XLSX
- Putni-nalog detail modal: novi gumb "📄 HUB-3 uplatnica (PDF)"
- klub selector bonus za stats tab
Live tests (8/8):
- GET /erp → 200, 61.5 KB
- /api/erp/stats month=€63.15 / pn_year=€455
- /export/invoices.xlsx → 200, application/vnd.ms-excel, valid PK header
- /putni-nalog/1/hub3.pdf → 200, application/pdf 53562 B (%PDF-)
- /attach-invoice → ok, link_id=1
- /bulk-pay {ids:[1]} → skipped:1 (već plaćen)
- /bulk-cancel {ids:[999]} → 0/0 (ne postoji, tolerantno)
- Suggested invoices vraća praznu listu nakon attach
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-05 01:32:05 +02:00
Damir Radulić
0046b8d695
CC2 R5: defense-in-depth JWT + invite/reset token flows + audit
...
#1 JWT middleware:
- pgz_sport_api.py: starlette middleware require_jwt_on_admin runs before
every /api/admin/* route. Even routes that lack Depends(require_user)
cannot be reached without a valid Bearer token (verifies signature,
exp, typ='access', revocation via user_sessions). OPTIONS passes for CORS.
#2 Invitation flow:
- pgz_sport.user_action_tokens table (token_hash, user_id, kind, expires_at,
used_at, created_by, ip, meta). Single-use, raw token never persisted.
- POST /api/admin/users/{id}/invite — issues 'invite' token (TTL 7d),
marks must_change_pwd, revokes existing sessions, returns invite_link.
- GET /api/auth/setup-password?token=X — preflight (no consume).
- POST /api/auth/setup-password — consumes token, sets password, sets
email_verified=true.
#3 Password reset flow:
- POST /api/auth/forgot-password — generic 'ako račun postoji' response;
issues 'reset' token (TTL 2h) only for active users. Token returned in
response only on localhost or if PGZ_REVEAL_RESET_TOKEN=1.
- GET /api/auth/reset-password?token=X — preflight.
- POST /api/auth/reset-password — consumes token, sets new password,
revokes all active sessions.
#4 Audit coverage (auth events):
- login.ok, login.fail (with reason), login.locked, login.2fa_required,
login.2fa_fail, logout, auth.refresh, password.change, password.reset.ok,
password.reset.fail, password.forgot.issue, password.forgot.miss,
invite.consume.ok, invite.consume.fail, user.invite, user.create,
user.update, user.delete, user.role.change, user.suspend, user.unsuspend,
user.password.reset, 2fa.verify.ok, 2fa.verify.fail, 2fa.disable.
#5 Live tests: 41/41 across 6 demo users (incl. fresh invited+deleted user).
Phase 2 verifies 14 endpoints reject no-auth and accept valid Bearer.
2026-05-05 01:28:29 +02:00
Damir Radulić
f5c6570d47
CC2 R4 #2+#5: remove legacy unauth /api/admin/users — close 401 gap
...
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.
2026-05-05 00:44:50 +02:00
CC4-PGZ-Sport
834b7bf89f
M5.1 OCR upload + parse + invoices CRUD (ERP)
...
- erp/ocr.py: FastAPI router under /api/erp/*
- POST /ocr/upload: file → pgz_sport.invoice_uploads (sha256, mime, klub_id, tenant_id)
- POST /ocr/parse: Tesseract+pdftotext OCR + DeepSeek V3 LLM extraction
- GET/POST/PUT /invoices, /invoices/{id}/pay, uploads list
- Wired into pgz_sport_api.py
- HR invoice regex (OIB, IBAN, datum DD.MM.YYYY i ISO, ukupno/PDV)
- DeepSeek V3 returns JSON object {izdavatelj_*, kupac_*, iznos_neto/pdv/brutto, stavke[], vrsta_troska...}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-04 23:53:22 +02:00