Spaces:
Sleeping
Sleeping
File size: 7,361 Bytes
7b6762c 8b0d750 7b6762c 8b0d750 7b6762c 77e48ab 7b6762c 8b0d750 7b6762c 8b0d750 7b6762c 8b0d750 7b6762c 77e48ab 7b6762c 8b0d750 7b6762c 8b0d750 7b6762c 8b0d750 7b6762c 77e48ab | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | """``/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),
}
|