"""OAuth provider helpers. Each provider exposes: - exchange_code(code, redirect_uri) -> OAuthProfile """ from dataclasses import dataclass import httpx from app.core.config import settings @dataclass class OAuthProfile: provider_subject: str email: str name: str async def google_exchange_code(code: str, redirect_uri: str) -> OAuthProfile: async with httpx.AsyncClient(timeout=10) as cli: token_response = await cli.post( "https://oauth2.googleapis.com/token", data={ "code": code, "client_id": settings.GOOGLE_CLIENT_ID, "client_secret": settings.GOOGLE_CLIENT_SECRET, "redirect_uri": redirect_uri, "grant_type": "authorization_code", }, ) token_response.raise_for_status() access_token = token_response.json()["access_token"] userinfo = await cli.get( "https://openidconnect.googleapis.com/v1/userinfo", headers={"Authorization": f"Bearer {access_token}"}, ) userinfo.raise_for_status() data = userinfo.json() return OAuthProfile( provider_subject=data["sub"], email=data["email"], name=data.get("name") or data["email"].split("@")[0], ) async def microsoft_exchange_code(code: str, redirect_uri: str) -> OAuthProfile: async with httpx.AsyncClient(timeout=10) as cli: token_response = await cli.post( "https://login.microsoftonline.com/common/oauth2/v2.0/token", data={ "code": code, "client_id": settings.MS_CLIENT_ID, "client_secret": settings.MS_CLIENT_SECRET, "redirect_uri": redirect_uri, "grant_type": "authorization_code", "scope": "openid email profile", }, ) token_response.raise_for_status() access_token = token_response.json()["access_token"] userinfo = await cli.get( "https://graph.microsoft.com/v1.0/me", headers={"Authorization": f"Bearer {access_token}"}, ) userinfo.raise_for_status() data = userinfo.json() return OAuthProfile( provider_subject=data["id"], email=data.get("mail") or data["userPrincipalName"], name=data.get("displayName") or data["userPrincipalName"].split("@")[0], )