M1+M2+M10 (CC2 R3): JWT auth + admin users + GDPR backend

- auth/auth_v2.py: JWT login/refresh/logout/me + bcrypt + tenant_id/role/tier claims
- auth/admin_users.py: /api/admin/users CRUD + invite/role/suspend + bulk CSV
- auth/gdpr.py: cookie consent + Art.20 export + Art.17 erasure + admin queue
- auth/seed_demo.py: 3 demo tenants + 4 users (damir@pgz.hr / PGZ2026!)
- Removed legacy /api/auth/login + /api/auth/me from pgz_sport_api.py
- Wired auth/admin/gdpr routers into FastAPI

5/5 live curl tests pass: damir@pgz.hr login → JWT with tenant_id=1, role=pgz_admin, tier=0
This commit is contained in:
Damir Radulić
2026-05-05 00:09:09 +02:00
parent c12a8e9698
commit 492c8fdd87
23 changed files with 21518 additions and 49 deletions
+25 -39
View File
@@ -933,21 +933,7 @@ def google_auth(token: str = Body(..., embed=True)):
except Exception as e:
raise HTTPException(401, f"Google auth failed: {e}")
@app.get("/api/auth/me")
def auth_me(authorization: Optional[str] = Header(None)):
"""Get current user info from JWT."""
if not authorization: return {"role": "viewer", "email": None, "name": None}
token = authorization.replace("Bearer ", "").strip()
# Try JWT first
try:
payload = _jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
return {"role": payload.get("role"), "email": payload.get("email"), "name": payload.get("name")}
except Exception:
pass
# Legacy demo token
if token == ADMIN_TOKEN:
return {"role": "admin", "email": "demo@admin", "name": "Demo Admin"}
return {"role": "viewer", "email": None, "name": None}
# /api/auth/me handled by auth.auth_v2 router (M1)
# ==================== STATIC ====================
import pathlib
@@ -1422,6 +1408,29 @@ try:
except Exception as e:
print(f'[CRM/M9] obrasci router fail: {e}')
# === Round 3 / CC2 — M1 Auth + M2 Admin Users + M10 GDPR ===
try:
from auth.auth_v2 import router as auth_v2_router
app.include_router(auth_v2_router)
print('[AUTH/M1] auth_v2 router loaded (/api/auth/*)')
except Exception as e:
print(f'[AUTH/M1] auth_v2 router fail: {e}')
try:
from auth.admin_users import router as admin_users_router
app.include_router(admin_users_router)
print('[AUTH/M2] admin_users router loaded (/api/admin/users/*)')
except Exception as e:
print(f'[AUTH/M2] admin_users router fail: {e}')
try:
from auth.gdpr import router as gdpr_router, admin_router as gdpr_admin_router
app.include_router(gdpr_router)
app.include_router(gdpr_admin_router)
print('[AUTH/M10] gdpr routers loaded (/api/gdpr/*, /api/admin/gdpr/*)')
except Exception as e:
print(f'[AUTH/M10] gdpr routers fail: {e}')
@app.get("/sport-3d")
@@ -1511,30 +1520,7 @@ def get_user(token):
return payload
except: return None
# ── AUTH: Email/Password login ──────────────────────────────────
@app.post("/api/auth/login")
def login(body: dict = Body(...)):
email = (body.get("email","")).lower().strip()
pwd = body.get("password","")
if not email or not pwd: raise HTTPException(400,"Email i lozinka obavezni")
rows = fetch("SELECT * FROM pgz_sport.users WHERE LOWER(email)=%s AND aktivan=TRUE",[email])
if not rows: raise HTTPException(401,"Neispravni podaci")
u = rows[0]
ph = hashlib.sha256(pwd.encode()).hexdigest()
if u.get("password_hash") != ph: raise HTTPException(401,"Neispravni podaci")
payload = {"uid":u["id"],"email":email,"name":u.get("full_name",email),
"role":u.get("user_type","viewer"),"klub_id":u.get("klub_id"),
"savez_id":u.get("savez_id"),"iat":int(__import__("time").time()),
"exp":int(__import__("time").time())+86400*7}
tok = _jwt.encode(payload, JWT_SECRET, algorithm="HS256")
try:
with db() as conn:
cur=conn.cursor()
cur.execute("UPDATE pgz_sport.users SET last_login=NOW() WHERE id=%s",[u["id"]])
conn.commit()
except: pass
return {"token":tok,"role":payload["role"],"name":payload["name"],
"email":email,"klub_id":payload["klub_id"],"savez_id":payload["savez_id"]}
# ── AUTH: Email/Password login — handled by auth.auth_v2 router (M1) ──
# ── SPORTAS FULL PROFILE ─────────────────────────────────────────
@app.get("/api/sportas/{clan_id}/profil")