Spaces:
Running on Zero
Running on Zero
File size: 6,152 Bytes
34a66f3 | 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 | 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."}
|