from __future__ import annotations from urllib.parse import urlencode import httpx from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse from vocero_api.api.responses import validated_json_response from vocero_api.core import config as settings from vocero_api.core import logging as app_logging from vocero_api.models.responses import ReportsResponse, UserProfileResponse from vocero_api.providers import caller as providers_caller from vocero_api.services import context as services_context router = APIRouter() @router.get("/api/user/phone", response_model=UserProfileResponse) async def user_by_phone(phoneNumber: str) -> JSONResponse: """HTTP passthrough: fetches caller identity / profile from the external info endpoint set via VOCERO_USER_PHONE_AUTH_URL and forwards the raw JSON to the browser. The proxy NEVER inspects this payload nor injects it into the realtime WebSocket session. It is only used by: - the browser UI to render a caller name header (#ident-user-name) - the /api/reasoning HTTP body user_context field (browser's choice). When VOCERO_USER_PHONE_AUTH_URL is unset we return 503 so the UI can silently skip identity loading (backward compatible with generic deploys). When S2S_LOGIN_BY_PHONE is set, the proxy first performs a login-by-phone POST with the caller phone and (if a bearer token is extracted) forwards it as Authorization: Bearer on the subsequent profile GET. """ if settings.S2S_MCP_URL: raise HTTPException(409, "Usa la sesión de conversación para consultar AquaPortal.") if not settings.VOCERO_USER_PHONE_AUTH_URL: raise HTTPException( status_code=503, detail="VOCERO_USER_PHONE_AUTH_URL no está configurado en este despliegue.", ) phone = (phoneNumber or "").strip() if not phone: raise HTTPException(status_code=400, detail="Se requiere el campo phoneNumber.") # 0) Login-by-phone (optional — only if S2S_LOGIN_BY_PHONE env is set). login_token = None if settings.S2S_LOGIN_BY_PHONE: login_token, _login_obj = await providers_caller._login_by_phone(phone) target_url = f"{settings.VOCERO_USER_PHONE_AUTH_URL}?{urlencode({'phoneNumber': phone})}" app_logging.logger.debug( "[EXT][USER-BY-PHONE] GET upstream=%s phoneLen=%d bearer=%s", settings.VOCERO_USER_PHONE_AUTH_URL, len(phone), "yes" if login_token else "no", ) try: headers = {} if login_token: headers["Authorization"] = f"Bearer {login_token}" async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as http: resp = await http.get(target_url, headers=headers or None) except httpx.RequestError as exc: app_logging.logger.warning("[EXT][USER-BY-PHONE] unreachable: %r", exc) raise HTTPException(status_code=502, detail="No se puede conectar con el servicio de información del usuario.") # noqa: B904 # Forward the upstream status + JSON verbatim; never rewrite the payload. if resp.status_code >= 500: app_logging.logger.warning( "[EXT][USER-BY-PHONE] upstream HTTP %s: %.200s", resp.status_code, resp.text, ) try: payload = resp.json() except Exception: raise HTTPException( # noqa: B904 status_code=502, detail="El servicio de información del usuario devolvió un JSON no válido.", ) return validated_json_response( payload, UserProfileResponse, status_code=resp.status_code if resp.status_code != 204 else 200 ) @router.get("/api/user/reports", response_model=ReportsResponse) async def user_reports(phoneNumber: str | None = None) -> JSONResponse: """Fetch + normalize the scheduled reports list for the caller UI. Mirrors the same sequence used in the WebSocket connect handler and /api/reasoning: 0. login-by-phone (optional) → bearer token 1. GET S2S_REPORT_LIST with ?phoneNumber= (if phone provided) + Bearer auth 2. run the same resolver used for prompt injection: filter 3 fields, sort DESC, build hints + entries → JSON returned verbatim so the browser can render a "reports loaded" summary in the UI header. Returns 503 when S2S_REPORT_LIST is not configured (generic deploy with no reports feature enabled) so the browser silently skips UI rendering. Response shape on success: { "count": N, "wrappedLen": X, "hints": [ "• Report #1: '