223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
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'
|
|
# }
|
|
# )
|