Spaces:
Sleeping
Sleeping
| """``/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 | |
| 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", ""), | |
| ) | |
| 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) | |
| 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) | |
| 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), | |
| } | |