from __future__ import annotations import asyncio from typing import Any, cast from fastapi import HTTPException from supabase import AuthError, create_client from core.config.settings import SupabaseSettings, config from core.logging import get_logger from v1.auth.schemas import ( AuthResendCodeResponse, AuthSignupStartResponse, AuthTokenResponse, AuthUser, AuthUserByEmailResponse, LoginRequest, RefreshRequest, SignupResendRequest, SignupStartRequest, SignupVerifyRequest, ) from v1.auth.service import AuthServiceGateway logger = get_logger("v1.auth.gateway") class SupabaseAuthGateway(AuthServiceGateway): _client: Any _admin_client: Any def __init__(self) -> None: settings: SupabaseSettings = config.supabase self._client = create_client(settings.url, settings.anon_key) self._admin_client = create_client(settings.url, settings.service_role_key) async def signup_start( self, request: SignupStartRequest ) -> AuthSignupStartResponse: payload: dict[str, Any] = { "email": request.email, "password": request.password, "data": {"username": request.username}, } if request.redirect_to: payload["options"] = {"email_redirect_to": request.redirect_to} try: sign_up = cast(Any, self._client.auth.sign_up) await asyncio.to_thread(sign_up, payload) return AuthSignupStartResponse(email=request.email) except AuthError as exc: logger.warning("Signup failed", error_type=type(exc).__name__) raise HTTPException( status_code=422, detail="Invalid signup request" ) from exc async def signup_verify(self, request: SignupVerifyRequest) -> AuthTokenResponse: payload: dict[str, Any] = { "type": "signup", "email": request.email, "token": request.token, } try: verify_otp = cast(Any, self._client.auth.verify_otp) response = await asyncio.to_thread(verify_otp, payload) return _map_auth_response(response, "Invalid verification code") except AuthError as exc: logger.warning("Signup verify failed", error_type=type(exc).__name__) raise HTTPException( status_code=401, detail="Invalid verification code" ) from exc async def signup_resend( self, request: SignupResendRequest ) -> AuthResendCodeResponse: payload: dict[str, Any] = {"type": "signup", "email": request.email} try: resend = cast(Any, self._client.auth.resend) await asyncio.to_thread(resend, payload) except AuthError as exc: logger.warning("Signup resend failed", error_type=type(exc).__name__) return AuthResendCodeResponse() async def login(self, request: LoginRequest) -> AuthTokenResponse: payload: dict[str, Any] = {"email": request.email, "password": request.password} try: sign_in = cast(Any, self._client.auth.sign_in_with_password) response = await asyncio.to_thread(sign_in, payload) return _map_auth_response(response, "Invalid credentials") except AuthError as exc: logger.warning("Login failed", error_type=type(exc).__name__) raise HTTPException(status_code=401, detail="Invalid credentials") from exc async def refresh(self, request: RefreshRequest) -> AuthTokenResponse: try: response = await asyncio.to_thread( self._client.auth.refresh_session, request.refresh_token, ) return _map_auth_response(response, "Invalid refresh token") except AuthError as exc: logger.warning("Refresh failed", error_type=type(exc).__name__) raise HTTPException( status_code=401, detail="Invalid refresh token" ) from exc async def logout(self, refresh_token: str | None) -> None: if not refresh_token: raise HTTPException(status_code=401, detail="Missing refresh token") try: response = await asyncio.to_thread( self._client.auth.refresh_session, refresh_token, ) session = getattr(response, "session", None) if session is None: raise HTTPException(status_code=401, detail="Invalid refresh token") await asyncio.to_thread( self._client.auth.set_session, str(session.access_token), str(session.refresh_token), ) await asyncio.to_thread(self._client.auth.sign_out) except AuthError as exc: logger.warning("Logout failed", error_type=type(exc).__name__) raise HTTPException( status_code=401, detail="Invalid refresh token" ) from exc async def get_user_by_email(self, email: str) -> AuthUserByEmailResponse: users = await asyncio.to_thread(_list_auth_users, self._admin_client) normalized_email = email.lower() user = next( ( candidate for candidate in users if str(getattr(candidate, "email", "")).lower() == normalized_email ), None, ) if user is None: raise HTTPException(status_code=404, detail="User not found") return AuthUserByEmailResponse( id=str(getattr(user, "id", "")), email=str(getattr(user, "email", "")), created_at=str(getattr(user, "created_at", "")), email_confirmed_at=( str(getattr(user, "email_confirmed_at", "")) if getattr(user, "email_confirmed_at", None) else None ), ) def _map_auth_response(response: object, failure_message: str) -> AuthTokenResponse: session = getattr(response, "session", None) user = getattr(response, "user", None) if session is None or user is None: raise HTTPException(status_code=401, detail=failure_message) email = getattr(user, "email", None) if not email: raise HTTPException(status_code=401, detail=failure_message) auth_user = AuthUser(id=str(user.id), email=str(email)) return AuthTokenResponse( access_token=str(session.access_token), refresh_token=str(session.refresh_token), expires_in=int(session.expires_in or 0), token_type=str(session.token_type), user=auth_user, ) def _list_auth_users(client: Any) -> list[Any]: users: list[Any] = [] page = 1 while True: response = client.auth.admin.list_users(page=page, per_page=100) batch = list(getattr(response, "users", [])) users.extend(batch) if len(batch) < 100: break page += 1 return users