Initial commit
This commit is contained in:
9
src/presentation/routing/__init__.py
Normal file
9
src/presentation/routing/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
from src.presentation.routing.auth import auth_router
|
||||
from src.presentation.routing.csrf import csrf_router
|
||||
from src.presentation.routing.jwt import jwt_router
|
||||
|
||||
v1_router = APIRouter(prefix='/v1')
|
||||
v1_router.include_router(auth_router)
|
||||
v1_router.include_router(csrf_router)
|
||||
v1_router.include_router(jwt_router)
|
||||
222
src/presentation/routing/auth.py
Normal file
222
src/presentation/routing/auth.py
Normal 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'
|
||||
# }
|
||||
# )
|
||||
37
src/presentation/routing/csrf.py
Normal file
37
src/presentation/routing/csrf.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from starlette import status
|
||||
from src.infrastructure.security import CsrfService
|
||||
from src.infrastructure.config import settings
|
||||
from src.presentation.decorators import rate_limit
|
||||
|
||||
csrf_router = APIRouter(prefix='/csrf', tags=['csrf'])
|
||||
|
||||
|
||||
@csrf_router.get('/token', 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 issue_csrf_token():
|
||||
csrf = CsrfService()
|
||||
|
||||
token = csrf.issue()
|
||||
|
||||
response = ORJSONResponse(
|
||||
content={
|
||||
'token': token,
|
||||
'header_name': csrf.header_name,
|
||||
}
|
||||
)
|
||||
|
||||
response.set_cookie(
|
||||
key=csrf.cookie_name,
|
||||
value=token,
|
||||
secure=settings.CSRF_COOKIE_SECURE,
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY,
|
||||
samesite=settings.CSRF_COOKIE_SAMESITE,
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
domain=settings.CSRF_COOKIE_DOMAIN,
|
||||
max_age=csrf.ttl_seconds,
|
||||
)
|
||||
|
||||
return response
|
||||
64
src/presentation/routing/jwt.py
Normal file
64
src/presentation/routing/jwt.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from starlette import status
|
||||
from src.application.commands import JwtRefreshCommand
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.config import settings
|
||||
from src.presentation.decorators import rate_limit
|
||||
from src.presentation.dependencies import get_jwt_refresh_command
|
||||
|
||||
|
||||
jwt_router = APIRouter(prefix='/jwt', tags=['Jwt'])
|
||||
|
||||
|
||||
@jwt_router.post('/refresh', 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 refresh_tokens(
|
||||
request: Request,
|
||||
command: JwtRefreshCommand = Depends(get_jwt_refresh_command)
|
||||
):
|
||||
refresh_token = request.cookies.get('refresh_token')
|
||||
|
||||
if not refresh_token:
|
||||
response = ORJSONResponse({'ok': False, 'error': 'No refresh token'}, status_code=401)
|
||||
response.delete_cookie('access_token', path='/')
|
||||
response.delete_cookie('refresh_token', path='/')
|
||||
return response
|
||||
|
||||
ip = request.client.host if request.client else None
|
||||
user_agent = request.headers.get('user-agent')
|
||||
|
||||
try:
|
||||
access, refresh = await command(refresh_token=refresh_token, ip=ip, user_agent=user_agent)
|
||||
except ApplicationException:
|
||||
response = ORJSONResponse({'result': False}, status_code=401)
|
||||
response.delete_cookie('access_token', path='/')
|
||||
response.delete_cookie('refresh_token', path='/')
|
||||
return response
|
||||
|
||||
response = ORJSONResponse({'result': True})
|
||||
|
||||
response.set_cookie(
|
||||
key='access_token',
|
||||
value=access,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite='lax',
|
||||
path='/',
|
||||
max_age=int(settings.JWT_ACCESS_TTL_SECONDS),
|
||||
)
|
||||
response.set_cookie(
|
||||
key='refresh_token',
|
||||
value=refresh,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite='lax',
|
||||
path='/',
|
||||
max_age=int(settings.JWT_REFRESH_TTL_SECONDS),
|
||||
)
|
||||
return response
|
||||
|
||||
# Usage
|
||||
# @jwt_router.get("/test")
|
||||
# async def profile(auth: AuthContext = Depends(require_access_token)):
|
||||
# return 'ok'
|
||||
Reference in New Issue
Block a user