CyberArena / app /api /profile.py
Hussien Haider
H
77e48ab
Raw
History Blame Contribute Delete
7.36 kB
"""``/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),
}