from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, Request, Response from fastapi import HTTPException from core.auth.models import CurrentUser from v1.auth.rate_limit import enforce_rate_limit from v1.auth.dependencies import get_auth_service from v1.users.dependencies import get_current_user from v1.auth.schemas import ( PasswordResetConfirmRequest, SessionCreateRequest, SessionDeleteRequest, SessionRefreshRequest, SessionResponse, UserByEmailResponse, VerificationCreateRequest, VerificationCreateResponse, VerificationResendRequest, VerificationVerifyRequest, ) from v1.auth.service import AuthService router = APIRouter(prefix="/auth", tags=["auth"]) @router.post( "/verifications", response_model=VerificationCreateResponse, status_code=202 ) async def create_verification( payload: VerificationCreateRequest, service: AuthService = Depends(get_auth_service), ) -> VerificationCreateResponse: await enforce_rate_limit( scope="signup_start", identifier=payload.email, limit=5, window_seconds=60, ) return await service.create_verification(payload) @router.post("/verify", response_model=SessionResponse) async def verify( payload: VerificationVerifyRequest, request: Request, service: AuthService = Depends(get_auth_service), ) -> SessionResponse | Response: scope = "signup_verify" if payload.type == "signup" else "password_reset_confirm" limit = 10 window_seconds = 600 await enforce_rate_limit( scope=scope, identifier=f"{payload.email.lower()}:{_client_ip(request)}", limit=limit, window_seconds=window_seconds, ) if payload.type == "signup": return await service.verify_verification(payload) if payload.new_password is None: raise HTTPException(status_code=422, detail="Invalid request") await service.confirm_password_reset( PasswordResetConfirmRequest( email=payload.email, token=payload.token, new_password=payload.new_password, ) ) return Response(status_code=204) @router.post("/resend", status_code=204) async def resend( payload: VerificationResendRequest, request: Request, service: AuthService = Depends(get_auth_service), ) -> Response: scope = "signup_resend" if payload.type == "signup" else "password_reset_request" await enforce_rate_limit( scope=scope, identifier=f"{payload.email.lower()}:{_client_ip(request)}", limit=5, window_seconds=60, ) await service.resend_verification(payload) return Response(status_code=204) @router.post("/sessions", response_model=SessionResponse) async def create_session( payload: SessionCreateRequest, service: AuthService = Depends(get_auth_service), ) -> SessionResponse: await enforce_rate_limit( scope="login", identifier=payload.email, limit=10, window_seconds=60, ) return await service.create_session(payload) @router.post("/sessions/refresh", response_model=SessionResponse) async def refresh_session( payload: SessionRefreshRequest, request: Request, service: AuthService = Depends(get_auth_service), ) -> SessionResponse: await enforce_rate_limit( scope="refresh", identifier=_client_ip(request), limit=10, window_seconds=60, ) return await service.refresh_session(payload) @router.delete("/sessions", status_code=204) async def delete_session( payload: SessionDeleteRequest, request: Request, service: AuthService = Depends(get_auth_service), ) -> Response: await enforce_rate_limit( scope="logout", identifier=_client_ip(request), limit=10, window_seconds=60, ) await service.delete_session(payload.refresh_token) return Response(status_code=204) def _client_ip(request: Request) -> str: forwarded_for = request.headers.get("x-forwarded-for", "") if forwarded_for: first = forwarded_for.split(",")[0].strip() if first: return first real_ip = request.headers.get("x-real-ip", "").strip() if real_ip: return real_ip host = request.client.host if request.client else "" return host or "unknown"