| import logging |
| from typing import Annotated |
| from urllib.parse import urlencode |
|
|
| import httpx |
| from fastapi import APIRouter, Depends, Query |
| from fastapi.responses import RedirectResponse |
|
|
| from src.api.dependencies import CurrentUserDep, DbSessionDep |
| from src.models.auth import ( |
| CurrentUserResponse, |
| LoginRequest, |
| LogoutRequest, |
| RefreshTokenRequest, |
| RegisterRequest, |
| TokenResponse, |
| ) |
| from src.services.auth import AuthService, AuthServiceError |
| from src.services.security import ( |
| SecurityError, |
| build_google_authorization_url, |
| create_oauth_state, |
| exchange_google_code, |
| verify_google_id_token, |
| verify_oauth_state, |
| ) |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(prefix="/auth", tags=["auth"]) |
|
|
|
|
| def get_auth_service(db: DbSessionDep) -> AuthService: |
| return AuthService(db) |
|
|
|
|
| AuthServiceDep = Annotated[AuthService, Depends(get_auth_service)] |
|
|
|
|
| def build_frontend_auth_redirect(callback_url: str, token_response: TokenResponse) -> str: |
| fragment = urlencode( |
| { |
| "access_token": token_response.access_token, |
| "refresh_token": token_response.refresh_token, |
| "token_type": token_response.token_type, |
| "access_token_expires_in": token_response.access_token_expires_in, |
| } |
| ) |
| separator = "&" if "#" in callback_url else "#" |
| return f"{callback_url}{separator}{fragment}" |
|
|
|
|
| @router.post("/register") |
| def register(payload: RegisterRequest, auth_service: AuthServiceDep) -> TokenResponse: |
| return auth_service.register( |
| email=payload.email, |
| password=payload.password, |
| full_name=payload.full_name, |
| ) |
|
|
|
|
| @router.post("/login") |
| def login(payload: LoginRequest, auth_service: AuthServiceDep) -> TokenResponse: |
| return auth_service.login(email=payload.email, password=payload.password) |
|
|
|
|
| @router.post("/refresh") |
| def refresh(payload: RefreshTokenRequest, auth_service: AuthServiceDep) -> TokenResponse: |
| return auth_service.refresh(payload.refresh_token) |
|
|
|
|
| @router.post("/logout") |
| def logout(payload: LogoutRequest, auth_service: AuthServiceDep) -> dict[str, str]: |
| auth_service.logout(payload.refresh_token) |
| return {"status": "ok"} |
|
|
|
|
| @router.get("/me") |
| def me(current_user: CurrentUserDep, auth_service: AuthServiceDep) -> CurrentUserResponse: |
| return auth_service.current_user_response(current_user) |
|
|
|
|
| @router.get("/google/login") |
| def google_login(auth_service: AuthServiceDep) -> RedirectResponse: |
| settings = auth_service.settings |
| if not settings.google_client_id or not settings.google_client_secret: |
| raise AuthServiceError(503, "auth_unavailable", "Google OAuth is not configured.") |
| state = create_oauth_state(settings) |
| return RedirectResponse(build_google_authorization_url(settings, state)) |
|
|
|
|
| @router.get("/google/callback") |
| async def google_callback( |
| code: Annotated[str, Query(min_length=1)], |
| state: Annotated[str, Query(min_length=1)], |
| auth_service: AuthServiceDep, |
| ) -> RedirectResponse: |
| settings = auth_service.settings |
| try: |
| verify_oauth_state(state, settings) |
| token_payload = await exchange_google_code(code, settings) |
| claims = verify_google_id_token(str(token_payload.get("id_token") or ""), settings) |
| except httpx.HTTPStatusError as exc: |
| try: |
| google_error = exc.response.json() |
| except ValueError: |
| google_error = {"body": exc.response.text[:500]} |
|
|
| logger.exception( |
| "Google OAuth token exchange failed: status=%s error=%s redirect_uri=%s client_id_suffix=%s", |
| exc.response.status_code, |
| google_error, |
| settings.google_redirect_uri, |
| settings.google_client_id[-12:], |
| ) |
| raise AuthServiceError(401, "unauthorized", "Google OAuth failed.") from exc |
|
|
| except (SecurityError, ValueError, httpx.HTTPError) as exc: |
| logger.exception( |
| "Google OAuth failed after callback: error_type=%s message=%s redirect_uri=%s client_id_suffix=%s", |
| type(exc).__name__, |
| str(exc), |
| settings.google_redirect_uri, |
| settings.google_client_id[-12:], |
| ) |
| raise AuthServiceError(401, "unauthorized", "Google OAuth failed.") from exc |
| token_response = auth_service.upsert_google_user(claims) |
| |
| |
| return RedirectResponse( |
| build_frontend_auth_redirect( |
| settings.frontend_auth_callback_url, |
| token_response, |
| ) |
| ) |
|
|