Initial commit

This commit is contained in:
2026-04-12 09:16:16 +03:00
commit 5fe8efc5d4
98 changed files with 5351 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
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=True,
samesite='lax',
path='/',
max_age=60 * 60 * 24 * 365 * 5
)
response.set_cookie(
key='access_token',
value=created.access_token,
httponly=True,
secure=True,
samesite='lax',
path='/',
max_age=int(settings.JWT_ACCESS_TTL_SECONDS),
)
response.set_cookie(
key='refresh_token',
value=created.refresh_token,
httponly=True,
secure=True,
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=True,
samesite='lax',
path='/',
max_age=60 * 60 * 24 * 365 * 5
)
response.set_cookie(
key='access_token',
value=dto.access_token,
httponly=True,
secure=True,
samesite='lax',
path='/',
max_age=int(settings.JWT_ACCESS_TTL_SECONDS),
)
response.set_cookie(
key='refresh_token',
value=dto.refresh_token,
httponly=True,
secure=True,
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'
# }
# )