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."}