from fastapi import APIRouter, Depends, status, Request from fastapi.responses import ORJSONResponse from ulid import ULID from src.application.commands import ( UserLogoutCommand, UserRegistrationStartCommand, UserLoginStartCommand, UserRegistrationCompleteCommand, UserLoginCompleteCommand ) from src.application.contracts import ILogger from src.application.domain.dto import UserLoginDto from src.infrastructure.config import settings from src.infrastructure.logger import get_logger from src.presentation.decorators import rate_limit, email_rl_key from src.presentation.dependencies import ( get_user_registration_complete_command, get_user_logout_command, get_user_registration_start_command, get_user_login_start_command, get_user_login_complete_command ) from src.presentation.schemas import UserLogin, RegistrationStart, RegistrationComplete, LoginStart #from src.presentation.decorators import csrf_protect auth_router = APIRouter(prefix='/auth', tags=['auth']) @auth_router.post( path='/registration/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK, ) @rate_limit(limit=5, window_seconds=60, scope='ip') @rate_limit(limit=3, window_seconds=600, scope='key', key_prefix='rl:reg_start', key_builder=email_rl_key) async def registration_start( request: Request, body: RegistrationStart, command: UserRegistrationStartCommand = Depends(get_user_registration_start_command), ): result = await command(body.email) return {'success': result} @auth_router.post(path='/registration/complete', response_class=ORJSONResponse, status_code=status.HTTP_201_CREATED) @rate_limit(limit=10, window_seconds=300, scope='ip') async def registration( request: Request, user: RegistrationComplete, command: UserRegistrationCompleteCommand = Depends(get_user_registration_complete_command), logger: ILogger = Depends(get_logger), ): device_id = request.cookies.get('device_id') if not device_id: device_id = str(ULID()) xff = request.headers.get('x-forwarded-for') ip = xff.split(',')[0].strip() if xff else (request.client.host if request.client else None) user_agent = request.headers.get('user-agent') created = await command( email=str(user.email), password=user.password, device_id=device_id, code=user.code, user_agent=user_agent, ip=ip, ) logger.info(f'Registration completed for user_id={created.id}') response = ORJSONResponse(content={'id': created.id, 'email': created.email}) response.set_cookie( key='device_id', value=device_id, httponly=True, secure=settings.AUTH_COOKIE_SECURE, samesite='lax', path='/', max_age=60 * 60 * 24 * 365 * 5 ) response.set_cookie( key='access_token', value=created.access_token, httponly=True, secure=settings.AUTH_COOKIE_SECURE, samesite='lax', path='/', max_age=int(settings.JWT_ACCESS_TTL_SECONDS), ) response.set_cookie( key='refresh_token', value=created.refresh_token, httponly=True, secure=settings.AUTH_COOKIE_SECURE, samesite='lax', path='/', max_age=int(settings.JWT_REFRESH_TTL_SECONDS), ) return response @auth_router.post(path='/login/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK) @rate_limit(limit=5, window_seconds=60, scope='ip') @rate_limit(limit=3, window_seconds=600, scope='key', key_prefix='rl:login_start', key_builder=email_rl_key) async def login_start( request: Request, body: LoginStart, command: UserLoginStartCommand = Depends(get_user_login_start_command), ): result = await command(body.email) return {'success': result} @auth_router.post(path='/login/compete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK) @rate_limit(limit=10, window_seconds=300, scope='ip') async def login( request: Request, user: UserLogin, command: UserLoginCompleteCommand = Depends(get_user_login_complete_command), logger: ILogger = Depends(get_logger) ): device_id = request.cookies.get('device_id') if not device_id: device_id = str(ULID()) xff = request.headers.get('x-forwarded-for') ip = xff.split(',')[0].strip() if xff else (request.client.host if request.client else None) user_agent = request.headers.get('user-agent') dto: UserLoginDto = await command( email=str(user.email), password=user.password, code=user.code, device_id=device_id, user_agent=user_agent, ip=ip, ) logger.info(f'Login completed for user_id={dto.id}') response = ORJSONResponse( content={ 'id': dto.id, 'email': dto.email, 'first_name': dto.first_name, 'middle_name': dto.middle_name, 'last_name': dto.last_name, 'birth_date': dto.birth_date.isoformat() if dto.birth_date else None, 'crypto_wallet': dto.crypto_wallet, 'phone': dto.phone, 'bik': dto.bik, 'account_number': dto.account_number, 'card_number': dto.card_number, 'inn': dto.inn, 'kyc_verified': dto.kyc_verified, 'kyc_verified_at': dto.kyc_verified_at, 'created_at': dto.created_at.isoformat() if dto.created_at else None, 'updated_at': dto.updated_at.isoformat() if dto.updated_at else None, } ) response.set_cookie( key='device_id', value=device_id, httponly=True, secure=settings.AUTH_COOKIE_SECURE, samesite='lax', path='/', max_age=60 * 60 * 24 * 365 * 5 ) response.set_cookie( key='access_token', value=dto.access_token, httponly=True, secure=settings.AUTH_COOKIE_SECURE, samesite='lax', path='/', max_age=int(settings.JWT_ACCESS_TTL_SECONDS), ) response.set_cookie( key='refresh_token', value=dto.refresh_token, httponly=True, secure=settings.AUTH_COOKIE_SECURE, samesite='lax', path='/', max_age=int(settings.JWT_REFRESH_TTL_SECONDS), ) return response @auth_router.post(path='/logout', response_class=ORJSONResponse, status_code=status.HTTP_200_OK) @rate_limit(limit=settings.RATE_LIMIT_REQUESTS, window_seconds=settings.RATE_LIMIT_WINDOW, scope='ip') async def logout_current( request: Request, command: UserLogoutCommand = Depends(get_user_logout_command), ): refresh_token = request.cookies.get('refresh_token') await command(refresh_token=refresh_token) response = ORJSONResponse({'ok': True}) response.delete_cookie('access_token', path='/') response.delete_cookie('refresh_token', path='/') return response # @auth_router.get(path='/ping') # @csrf_protect() # async def ping(request: Request): # return ORJSONResponse( # content={ # 'status': 'pong' # } # )