diff --git a/auth/auth_v2.py b/auth/auth_v2.py index 65b7ffe..fc313fe 100644 --- a/auth/auth_v2.py +++ b/auth/auth_v2.py @@ -391,7 +391,8 @@ def logout(request: Request, user = Depends(require_user)): def me(user = Depends(require_user)): enriched = db_one("""SELECT id, email, full_name, ime, prezime, user_type, klub_id, savez_id, must_change_pwd, aktivan, status, - last_login, oib, telefon, phone, preferred_language, created_at + last_login, oib, telefon, phone, preferred_language, created_at, + avatar_url, gdpr_consent_at, google_picture FROM pgz_sport.users WHERE id=%s""", (user["id"],)) if not enriched: raise HTTPException(404, "User not found") @@ -399,11 +400,94 @@ def me(user = Depends(require_user)): roles = db_query("""SELECT r.code, r.naziv, ur.scope_type, ur.scope_id FROM pgz_sport.user_roles ur JOIN pgz_sport.roles r ON r.id=ur.role_id WHERE ur.user_id=%s AND ur.active=true""", (user["id"],)) + try: + twofa = db_one("SELECT secret IS NOT NULL AS enabled FROM pgz_sport.user_2fa WHERE user_id=%s", + (user["id"],)) or {"enabled": False} + except Exception: + twofa = {"enabled": False} return {**enriched, "tier": _tier_for(enriched.get("user_type") or ""), "must_change_pwd": bool(enriched.get("must_change_pwd")), + "two_factor_enabled": bool(twofa.get("enabled")), **tenant, "roles": roles} +class UpdateMeReq(BaseModel): + ime: Optional[str] = None + prezime: Optional[str] = None + full_name: Optional[str] = None + telefon: Optional[str] = None + phone: Optional[str] = None + preferred_language: Optional[str] = None + oib: Optional[str] = None + +@router.put("/me") +def update_me(req: UpdateMeReq, request: Request, user = Depends(require_user)): + fields = [] + vals: List[Any] = [] + for k in ("ime","prezime","full_name","telefon","phone","preferred_language","oib"): + v = getattr(req, k) + if v is not None: + fields.append(f"{k}=%s") + vals.append(v.strip() if isinstance(v, str) else v) + if not fields: + raise HTTPException(400, "Nema polja za ažuriranje") + vals.append(user["id"]) + db_exec(f"UPDATE pgz_sport.users SET {', '.join(fields)}, updated_at=now() WHERE id=%s", tuple(vals)) + ip, ua = _client(request) + audit(user["id"], "profile.update", meta={"fields": [f.split("=")[0] for f in fields]}, ip=ip, ua=ua) + return me(user) + +# ─────────────────────────── AVATAR UPLOAD ─────────────────────────── +import shutil, pathlib +from fastapi import UploadFile, File + +UPLOAD_ROOT = pathlib.Path("/opt/pgz-sport/uploads") +AVATAR_DIR = UPLOAD_ROOT / "avatars" +AVATAR_DIR.mkdir(parents=True, exist_ok=True) +ALLOWED_AVATAR_MIME = {"image/jpeg","image/jpg","image/png","image/webp"} +ALLOWED_AVATAR_EXT = {".jpg",".jpeg",".png",".webp"} +MAX_AVATAR_BYTES = 5 * 1024 * 1024 # 5 MB + +@router.post("/me/avatar") +async def upload_my_avatar(request: Request, file: UploadFile = File(...), user = Depends(require_user)): + ct = (file.content_type or "").lower() + if ct not in ALLOWED_AVATAR_MIME: + raise HTTPException(400, f"Nedozvoljen tip slike: {ct} — jpeg/png/webp") + ext = pathlib.Path(file.filename or "").suffix.lower() + if ext not in ALLOWED_AVATAR_EXT: + ext = {"image/jpeg":".jpg","image/jpg":".jpg","image/png":".png","image/webp":".webp"}.get(ct, ".jpg") + data = await file.read() + if len(data) > MAX_AVATAR_BYTES: + raise HTTPException(413, f"Slika prevelika ({len(data)} B > {MAX_AVATAR_BYTES})") + if len(data) < 32: + raise HTTPException(400, "Slika prazna ili neispravna") + safe_name = f"{int(user['id'])}_{int(time.time())}{ext}" + target = AVATAR_DIR / safe_name + with open(target, "wb") as f: + f.write(data) + try: os.chmod(target, 0o644) + except Exception: pass + avatar_url = f"/uploads/avatars/{safe_name}" + db_exec("UPDATE pgz_sport.users SET avatar_url=%s, updated_at=now() WHERE id=%s", + (avatar_url, user["id"])) + ip, ua = _client(request) + audit(user["id"], "profile.avatar_upload", + meta={"file": safe_name, "size": len(data), "mime": ct}, ip=ip, ua=ua) + return {"status":"ok", "avatar_url": avatar_url, "size": len(data), "mime": ct} + +@router.delete("/me/avatar") +def delete_my_avatar(request: Request, user = Depends(require_user)): + cur = db_one("SELECT avatar_url FROM pgz_sport.users WHERE id=%s", (user["id"],)) + if cur and cur.get("avatar_url"): + p = AVATAR_DIR / pathlib.Path(cur["avatar_url"]).name + try: + if p.exists() and p.is_relative_to(AVATAR_DIR): p.unlink() + except Exception: pass + db_exec("UPDATE pgz_sport.users SET avatar_url=NULL, updated_at=now() WHERE id=%s", (user["id"],)) + ip, ua = _client(request) + audit(user["id"], "profile.avatar_delete", ip=ip, ua=ua) + return {"status": "ok"} + @router.post("/password/change") def change_password(req: ChangePwdReq, request: Request, user = Depends(require_user)): if len(req.new_password) < 8: diff --git a/pgz_sport_api.py b/pgz_sport_api.py index 5a41bb2..ffe80f1 100644 --- a/pgz_sport_api.py +++ b/pgz_sport_api.py @@ -1683,8 +1683,34 @@ def serve_platform(): if p.exists(): return FileResponse(p) return {"error": "platform.html not found"} + +@app.get("/app") +@app.get("/app/") +def serve_app(): + p = HTML_DIR / "app.html" + return FileResponse(p) if p.exists() else {"error":"app.html not found"} + +@app.get("/audit") +@app.get("/audit/") +def serve_audit(): + p = HTML_DIR / "audit.html" + return FileResponse(p) if p.exists() else {"error":"audit.html not found"} + +@app.get("/kpi") +@app.get("/kpi/") +def serve_kpi(): + p = HTML_DIR / "kpi.html" + return FileResponse(p) if p.exists() else {"error":"kpi.html not found"} + app.mount("/static", StaticFiles(directory=str(HTML_DIR)), name="static") +# User-uploaded files (avatars, etc.) — served at /uploads/* +import pathlib as _pl +_UPLOAD_DIR = _pl.Path("/opt/pgz-sport/uploads") +_UPLOAD_DIR.mkdir(parents=True, exist_ok=True) +(_UPLOAD_DIR / "avatars").mkdir(parents=True, exist_ok=True) +app.mount("/uploads", StaticFiles(directory=str(_UPLOAD_DIR)), name="uploads") + @app.get("/") def root(request: Request): host = request.headers.get("host", "")