Pant0x's picture
Add HF Space Gradio SDK backend (free tier)
34a66f3
Raw
History Blame Contribute Delete
6.15 kB
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from fastapi import APIRouter, Body, Header, HTTPException
import requests
from app.core.config import (
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_TIMEOUT_SECONDS,
PHONE_PATTERN, PROFILE_SELECT_FIELDS, supabase_configured, LOGGER,
)
from app.security.auth import require_authenticated_user, _supabase_headers, _normalize_spaces
from app.services.supabase_client import supabase_get_profile_by_user_id, supabase_get_profile_by_field
from app.api.helpers import (
_resolve_profile_name_fields, _compose_full_name, _missing_schema_hint,
)
router = APIRouter(prefix="/profile", tags=["profile"])
@router.put("/update")
def profile_update(
payload: Dict[str, Any] = Body(...),
authorization: Optional[str] = Header(default=None),
) -> Dict[str, Any]:
if not supabase_configured():
raise HTTPException(status_code=503, detail="Supabase is not configured.")
request_user = require_authenticated_user(authorization)
if request_user is None:
raise HTTPException(status_code=401, detail="Authentication is required.")
requester_id = str(request_user.get("id") or "").strip()
if not requester_id:
raise HTTPException(status_code=401, detail="Authenticated user id is missing.")
current_profile = supabase_get_profile_by_user_id(requester_id) or {}
current_first_name, current_last_name, _ = _resolve_profile_name_fields(
current_profile if isinstance(current_profile, dict) else None,
fallback_full_name=(current_profile or {}).get("full_name") if isinstance(current_profile, dict) else None,
)
name_fields_provided = any(key in payload for key in ["first_name", "firstName", "last_name", "lastName", "full_name", "name"])
requested_first_name, requested_last_name, _ = _resolve_profile_name_fields(payload)
email = _normalize_spaces(str(payload.get("email") or "")).lower()
phone_number = _normalize_spaces(str(payload.get("phone") or payload.get("phone_number") or ""))
profile_patch: Dict[str, Any] = {}
if name_fields_provided:
next_first_name = requested_first_name or current_first_name
next_last_name = requested_last_name or current_last_name
next_full_name = _compose_full_name(next_first_name, next_last_name)
if not next_full_name:
raise HTTPException(status_code=400, detail="Name update requires at least first_name or full_name.")
profile_patch["first_name"] = next_first_name
profile_patch["last_name"] = next_last_name
profile_patch["full_name"] = next_full_name
if email:
if "@" not in email:
raise HTTPException(status_code=400, detail="email format is invalid.")
other_with_email = supabase_get_profile_by_field("email", email, case_insensitive=True)
if isinstance(other_with_email, dict) and str(other_with_email.get("id") or "") != requester_id:
raise HTTPException(status_code=409, detail="email is already in use.")
profile_patch["email"] = email
if phone_number:
if not PHONE_PATTERN.fullmatch(phone_number):
raise HTTPException(status_code=400, detail="phone must be 7-15 digits and may start with +.")
other_with_phone = supabase_get_profile_by_field("phone_number", phone_number)
if isinstance(other_with_phone, dict) and str(other_with_phone.get("id") or "") != requester_id:
raise HTTPException(status_code=409, detail="phone is already in use.")
profile_patch["phone_number"] = phone_number
if not profile_patch:
raise HTTPException(status_code=400, detail="No profile fields were provided.")
response = requests.patch(
f"{SUPABASE_URL}/rest/v1/profiles",
params={"id": f"eq.{requester_id}", "select": PROFILE_SELECT_FIELDS},
json=profile_patch,
headers=_supabase_headers(api_key=SUPABASE_SERVICE_ROLE_KEY, bearer=SUPABASE_SERVICE_ROLE_KEY, content_type="application/json", prefer="return=representation"),
timeout=SUPABASE_TIMEOUT_SECONDS,
)
if response.status_code not in {200, 204}:
raise HTTPException(status_code=400, detail=f"Failed to update profile: {_missing_schema_hint(response.text)}")
updated_rows = response.json() if response.text else []
updated_profile = (
updated_rows[0]
if isinstance(updated_rows, list) and updated_rows and isinstance(updated_rows[0], dict)
else (supabase_get_profile_by_user_id(requester_id) or current_profile)
)
auth_patch: Dict[str, Any] = {}
user_metadata_patch: Dict[str, Any] = {}
if "email" in profile_patch:
auth_patch["email"] = profile_patch["email"]
if "first_name" in profile_patch:
user_metadata_patch["first_name"] = profile_patch["first_name"]
if "last_name" in profile_patch:
user_metadata_patch["last_name"] = profile_patch["last_name"]
if "full_name" in profile_patch:
user_metadata_patch["full_name"] = profile_patch["full_name"]
if "phone_number" in profile_patch:
user_metadata_patch["phone_number"] = profile_patch["phone_number"]
user_metadata_patch["phone"] = profile_patch["phone_number"]
if user_metadata_patch:
auth_patch["user_metadata"] = user_metadata_patch
if auth_patch:
try:
auth_response = requests.put(
f"{SUPABASE_URL}/auth/v1/admin/users/{requester_id}",
json=auth_patch,
headers={"apikey": SUPABASE_SERVICE_ROLE_KEY, "Authorization": f"Bearer {SUPABASE_SERVICE_ROLE_KEY}", "Content-Type": "application/json"},
timeout=SUPABASE_TIMEOUT_SECONDS,
)
if auth_response.status_code not in {200, 201}:
LOGGER.warning("Auth metadata update failed for %s: %s", requester_id, auth_response.text[:220])
except requests.RequestException as exc:
LOGGER.warning("Auth metadata update request failed for %s: %s", requester_id, exc)
return {"status": "ok", "profile": updated_profile, "message": "Profile updated successfully."}