Commit Graph

7 Commits

Author SHA1 Message Date
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