"""``/api/profile`` — user profile endpoints (avatar).""" import httpx from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Request from pydantic import BaseModel from app.core.auth import get_current_user, extract_token_from_request from app.core.config import SUPABASE_URL, SUPABASE_ANON_KEY router = APIRouter(prefix="/api/profile", tags=["profile"]) class AvatarResponse(BaseModel): avatar_url: str name: str email: str class AvatarUpdateResponse(BaseModel): avatar_url: str class AvatarUrlPayload(BaseModel): url: str @router.get("", response_model=AvatarResponse) async def get_profile( user: dict = Depends(get_current_user), request: Request = None, ): """Fetch the authenticated user's profile (avatar_url, name, email).""" user_id = user["user_id"] user_token = extract_token_from_request(request) or SUPABASE_ANON_KEY url = f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=id,avatar_url,name,email" async with httpx.AsyncClient(timeout=15) as client: resp = await client.get( url, headers={ "apikey": SUPABASE_ANON_KEY, "Authorization": f"Bearer {user_token}", }, ) if resp.status_code != 200 or not resp.json(): return AvatarResponse(avatar_url="", name=user.get("email", ""), email=user.get("email", "")) row = resp.json()[0] return AvatarResponse( avatar_url=row.get("avatar_url") or "", name=row.get("name") or user.get("email", ""), email=row.get("email") or user.get("email", ""), ) @router.post("/avatar-url", response_model=AvatarUpdateResponse) async def set_avatar_url( payload: AvatarUrlPayload, user: dict = Depends(get_current_user), request: Request = None, ): """Save an external avatar URL (e.g. from Google OAuth) to the user's profile.""" user_id = user["user_id"] user_token = extract_token_from_request(request) or SUPABASE_ANON_KEY update_url = f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}" async with httpx.AsyncClient(timeout=15) as client: await client.patch( update_url, json={"avatar_url": payload.url}, headers={ "apikey": SUPABASE_ANON_KEY, "Authorization": f"Bearer {user_token}", "Content-Type": "application/json", }, ) return AvatarUpdateResponse(avatar_url=payload.url) @router.post("/avatar", response_model=AvatarUpdateResponse) async def upload_avatar( file: UploadFile = File(...), user: dict = Depends(get_current_user), request: Request = None, ): """Upload a profile avatar for the authenticated user.""" user_id = user["user_id"] user_token = extract_token_from_request(request) or SUPABASE_ANON_KEY # Validate file type allowed = {"image/jpeg", "image/png", "image/webp", "image/gif"} if file.content_type not in allowed: raise HTTPException(status_code=400, detail="نوع الملف غير مدعوم. استخدم JPG, PNG, WebP أو GIF") # Validate file size (max 2MB) contents = await file.read() if len(contents) > 2 * 1024 * 1024: raise HTTPException(status_code=400, detail="حجم الملف يتجاوز 2MB") # Upload to Supabase Storage ext = file.filename.rsplit(".", 1)[-1] if "." in (file.filename or "") else "jpg" storage_path = f"{user_id}/avatar.{ext}" upload_url = f"{SUPABASE_URL}/storage/v1/object/avatars/{storage_path}" try: async with httpx.AsyncClient(timeout=30) as client: resp = await client.post( upload_url, content=contents, headers={ "apikey": SUPABASE_ANON_KEY, "Authorization": f"Bearer {SUPABASE_ANON_KEY}", "Content-Type": file.content_type, }, ) if resp.status_code not in (200, 201): # Try PUT if bucket requires overwrite resp = await client.put( upload_url, content=contents, headers={ "apikey": SUPABASE_ANON_KEY, "Authorization": f"Bearer {SUPABASE_ANON_KEY}", "Content-Type": file.content_type, }, ) if resp.status_code not in (200, 201): raise HTTPException( status_code=500, detail=f"فشل رفع الصورة: {resp.status_code} {resp.text}", ) except httpx.TimeoutException: raise HTTPException(status_code=504, detail="انتهت مهلة رفع الصورة") except Exception as e: raise HTTPException(status_code=500, detail=f"خطأ في رفع الصورة: {e}") public_url = f"{SUPABASE_URL}/storage/v1/object/public/avatars/{storage_path}" # Update user's avatar_url in the database update_url = f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}" try: async with httpx.AsyncClient(timeout=15) as client: await client.patch( update_url, json={"avatar_url": public_url}, headers={ "apikey": SUPABASE_ANON_KEY, "Authorization": f"Bearer {user_token}", "Content-Type": "application/json", }, ) except Exception: pass # Non-critical; the file is already uploaded return AvatarUpdateResponse(avatar_url=public_url) @router.get("/public/{user_id}") async def get_public_profile(user_id: str): """Fetch a public user profile (no auth required).""" headers = { "apikey": SUPABASE_ANON_KEY, "Authorization": f"Bearer {SUPABASE_ANON_KEY}", "Content-Type": "application/json", } async with httpx.AsyncClient(timeout=15) as client: url = f"{SUPABASE_URL}/rest/v1/leaderboard?id=eq.{user_id}&select=id,name,xp,completed_trainings,avatar_url&limit=1" resp = await client.get(url, headers=headers) user_data = {} if resp.status_code == 200 and resp.json(): user_data = resp.json()[0] else: url2 = f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=id,name,avatar_url&limit=1" resp2 = await client.get(url2, headers=headers) if resp2.status_code == 200 and resp2.json(): u = resp2.json()[0] user_data = {**u, "xp": 0, "completed_trainings": 0} cert_url = f"{SUPABASE_URL}/rest/v1/certificates?user_id=eq.{user_id}&select=id,category,issue_date,verify_code,title&order=issue_date.desc" cert_resp = await client.get(cert_url, headers=headers) certificates = cert_resp.json() if cert_resp.status_code == 200 else [] return { "user": { "id": user_data.get("id", user_id), "name": user_data.get("name") or "مستخدم", "xp": int(user_data.get("xp") or 0), "completed_trainings": int(user_data.get("completed_trainings") or 0), "avatar_url": user_data.get("avatar_url") or "", }, "certificates": certificates, "total_certificates": len(certificates), }