feat: add refresh

This commit is contained in:
2026-06-05 12:41:25 +03:00
parent d300d2eabe
commit 9d3c5b401a
8 changed files with 175 additions and 50 deletions

View File

@@ -2,11 +2,13 @@ from fastapi import APIRouter
from src.presentation.routing.auth import auth_router
from src.presentation.routing.documents import documents_router
from src.presentation.routing.jwt import jwt_router
from src.presentation.routing.organizations import organizations_router
from src.presentation.routing.purchase_requests import purchase_requests_router
v1_router = APIRouter(prefix='/v1')
v1_router.include_router(auth_router)
v1_router.include_router(jwt_router)
v1_router.include_router(organizations_router)
v1_router.include_router(documents_router)
v1_router.include_router(purchase_requests_router)

View File

@@ -1,29 +1,79 @@
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import ORJSONResponse
from src.application.commands import AdminLoginCommand, GetAdminMeCommand
from src.application.commands import AdminJwtRefreshCommand, AdminLoginCommand, GetAdminMeCommand
from src.application.domain.dto import AdminAuthContext
from src.application.domain.exceptions import ApplicationException, RefreshConcurrentException
from src.presentation.auth_cookies import set_auth_cookies, set_device_id_cookie
from src.presentation.decorators.admin_auth import require_admin_access
from src.presentation.dependencies.commands import get_admin_login_command, get_admin_me_command
from src.presentation.schemas.admin_auth import AdminLoginRequest, AdminLoginResponse, AdminMeResponse
from src.presentation.dependencies.commands import (
get_admin_jwt_refresh_command,
get_admin_login_command,
get_admin_me_command,
)
from src.presentation.schemas.admin_auth import (
AdminLoginRequest,
AdminLoginResponse,
AdminMeResponse,
AdminRefreshRequest,
AdminRefreshResponse,
)
auth_router = APIRouter(prefix='/auth', tags=['auth'])
def _client_ip(request: Request) -> str | None:
xff = request.headers.get('x-forwarded-for')
if xff:
return xff.split(',')[0].strip()
return request.client.host if request.client else None
@auth_router.post('/login', response_model=AdminLoginResponse, status_code=status.HTTP_200_OK)
async def admin_login(
body: AdminLoginRequest,
request: Request,
command: AdminLoginCommand = Depends(get_admin_login_command),
):
dto = await command(login=body.login, password=body.password)
return AdminLoginResponse(
access_token=dto.access_token,
id=dto.id,
login=dto.login,
first_name=dto.first_name,
last_name=dto.last_name,
role=dto.role,
dto = await command(
login=body.login,
password=body.password,
device_id=request.cookies.get('device_id'),
ip=_client_ip(request),
user_agent=request.headers.get('user-agent'),
)
response = ORJSONResponse(
AdminLoginResponse(
access_token=dto.access_token,
refresh_token=dto.refresh_token,
id=dto.id,
login=dto.login,
first_name=dto.first_name,
last_name=dto.last_name,
role=dto.role,
).model_dump()
)
set_auth_cookies(response, dto.access_token, dto.refresh_token)
set_device_id_cookie(response, dto.device_id)
return response
@auth_router.post('/refresh', response_model=AdminRefreshResponse, status_code=status.HTTP_200_OK)
async def admin_refresh(
body: AdminRefreshRequest,
request: Request,
command: AdminJwtRefreshCommand = Depends(get_admin_jwt_refresh_command),
):
try:
access, refresh = await command(
refresh_token=body.refresh_token,
ip=_client_ip(request),
user_agent=request.headers.get('user-agent'),
)
except RefreshConcurrentException:
raise ApplicationException(status_code=409, message='Refresh already in progress')
return AdminRefreshResponse(access_token=access, refresh_token=refresh)
@auth_router.post('/logout', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)

View File

@@ -4,38 +4,17 @@ from starlette import status
from src.application.commands import AdminJwtRefreshCommand
from src.application.domain.exceptions import ApplicationException, RefreshConcurrentException
from src.infrastructure.config import settings
from src.presentation.auth_cookies import clear_auth_cookies, set_auth_cookies
from src.presentation.dependencies.commands import get_admin_jwt_refresh_command
jwt_router = APIRouter(prefix='/jwt', tags=['jwt'])
def _clear_auth_cookies(response: ORJSONResponse) -> None:
response.delete_cookie('access_token', path='/', domain=settings.ADMIN_COOKIE_DOMAIN)
response.delete_cookie('refresh_token', path='/', domain=settings.ADMIN_COOKIE_DOMAIN)
def _set_auth_cookies(response: ORJSONResponse, access: str, refresh: str) -> None:
response.set_cookie(
key='access_token',
value=access,
httponly=True,
secure=settings.ADMIN_COOKIE_SECURE,
samesite='lax',
path='/',
domain=settings.ADMIN_COOKIE_DOMAIN,
max_age=int(settings.JWT_ACCESS_TTL_SECONDS),
)
response.set_cookie(
key='refresh_token',
value=refresh,
httponly=True,
secure=settings.ADMIN_COOKIE_SECURE,
samesite='lax',
path='/',
domain=settings.ADMIN_COOKIE_DOMAIN,
max_age=int(settings.JWT_REFRESH_TTL_SECONDS),
)
def _client_ip(request: Request) -> str | None:
xff = request.headers.get('x-forwarded-for')
if xff:
return xff.split(',')[0].strip()
return request.client.host if request.client else None
@jwt_router.post('/refresh', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
@@ -45,26 +24,26 @@ async def refresh_tokens(
):
refresh_token = request.cookies.get('refresh_token')
if not refresh_token:
response = ORJSONResponse({'ok': False, 'error': 'No refresh token'}, status_code=401)
_clear_auth_cookies(response)
response = ORJSONResponse({'result': False, 'error': 'No refresh token'}, status_code=401)
clear_auth_cookies(response)
return response
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')
try:
tokens = await command(refresh_token=refresh_token, ip=ip, user_agent=user_agent)
tokens = await command(
refresh_token=refresh_token,
ip=_client_ip(request),
user_agent=request.headers.get('user-agent'),
)
except RefreshConcurrentException:
return ORJSONResponse({'result': True, 'concurrent': True}, status_code=status.HTTP_200_OK)
except ApplicationException as exc:
if exc.status_code == status.HTTP_401_UNAUTHORIZED:
response = ORJSONResponse({'result': False}, status_code=401)
_clear_auth_cookies(response)
clear_auth_cookies(response)
return response
raise
access, refresh = tokens
response = ORJSONResponse({'result': True})
_set_auth_cookies(response, access, refresh)
set_auth_cookies(response, access, refresh)
return response