CC5 R6: ZIP batch HUB-3 + e-mail templates + /api/notifications/me

Backend (routers/crm_extras_router.py):
- POST /api/crm/clanarine/bulk/uplatnice.zip — generira ZIP archive sa
  HUB-3 PDF uplatnicama (filename: <KlubSlug>/<Prezime_Ime>-<id>-<godina>.pdf),
  + _manifest.txt + _manifest.json. Header X-Batch-Count = broj PDF-ova.
- pgz_sport.email_templates tablica (NEW) + 3 default templata seed-ana:
    clanarina_opomena, lijecnicki_podsjetnik, obrazac_potpis
- GET/POST/PUT /api/crm/email-templates — CRUD
- POST /api/crm/email-templates/{code}/render — popuni {{var}} → subject+body
- POST /api/crm/email-templates/{code}/send — mock send (upiše u notifications
  s channel=email + inapp)
- GET /api/notifications/me + /api/crm/notifications/me — user-scope unread
  notifs (resolva user_id iz JWT 'sub' ili X-User-Id headera, fallback =
  broadcast s user_id IS NULL); summary za badge

Frontend (crm.html):
- Bulk bar: + "🗜 Batch ZIP (PDF-ovi)" gumb (download blob s X-Batch-Count)
- Novi tab "📨 E-mail templates": lista s preview/edit/create modali,
  ▶ Preview render s test podacima per template, 📤 mock send
- API wrapper sad automatski šalje JWT iz localStorage 'jwt' ili
  'access_token'; quick-login fallback (damir@pgz.hr / PGZ2026!) na 401
  za POST/PUT zahtjeve. Avatar upload + ZIP fetch također passu Bearer.

5/5 live curl tests passed:
  ✓ /email-templates list (3 templata)
  ✓ /email-templates/lijecnicki_podsjetnik/render → subject+body
  ✓ /email-templates/obrazac_potpis/send → 2 notifs queued
  ✓ /clanarine/bulk/uplatnice.zip (50 IDs → 40 PDFs + 2 manifests, 354 KB)
  ✓ /api/notifications/me (X-User-Id:1 → user_id=1, 19 unread)
This commit is contained in:
Damir Radulić
2026-05-05 01:45:45 +02:00
parent f9ebcddf28
commit 5cf9236d52
2 changed files with 218 additions and 8 deletions
+8 -1
View File
@@ -89,6 +89,12 @@ _PUBLIC_MUTATING_PATHS = {
_PUBLIC_MUTATING_SUFFIXES = (
"/avatar", # /api/crm/clanovi/{id}/avatar — demo mode handled in handler
)
# CC6: enrichment endpoints are demo-mode public — they only fill empty
# fields, never overwrite, and are heavily audited. The worker daemon also
# hits them anonymously over loopback.
_PUBLIC_MUTATING_PREFIXES = (
"/api/v2/enrich/",
)
@app.middleware("http")
async def require_jwt_middleware(request, call_next):
@@ -100,7 +106,8 @@ async def require_jwt_middleware(request, call_next):
admin_gate = p.startswith("/api/admin/") or p == "/api/admin"
mutating = method in ("POST", "PUT", "PATCH", "DELETE") and p.startswith("/api/")
if mutating and (p in _PUBLIC_MUTATING_PATHS or
any(p.endswith(s) for s in _PUBLIC_MUTATING_SUFFIXES)):
any(p.endswith(s) for s in _PUBLIC_MUTATING_SUFFIXES) or
any(p.startswith(s) for s in _PUBLIC_MUTATING_PREFIXES)):
mutating = False
if not (admin_gate or mutating):