feat: add set phone
This commit is contained in:
@@ -1 +1,11 @@
|
|||||||
from src.application.domain.exceptions.application_exceptions import ApplicationException
|
from src.application.domain.exceptions.application_exceptions import (
|
||||||
|
ApplicationException,
|
||||||
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
ForbiddenException,
|
||||||
|
InternalException,
|
||||||
|
NotFoundException,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
TooManyRequestsException,
|
||||||
|
UnauthorizedException,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
|
||||||
|
|
||||||
@@ -14,5 +15,45 @@ class ApplicationException(Exception):
|
|||||||
self.message = message
|
self.message = message
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f"{self.status_code}: {self.message}"
|
return f'{self.status_code}: {self.message}'
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(400, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Unauthorized', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(401, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Forbidden', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(403, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Not found', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(404, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(409, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyRequestsException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(429, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailableException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(503, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class InternalException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Internal Server Error', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(500, message, headers)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from fastapi import status
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import ApplicationException, BadRequestException, InternalException, NotFoundException
|
||||||
from src.application.abstractions.repositories import IUserRepository
|
from src.application.abstractions.repositories import IUserRepository
|
||||||
from src.application.domain.entities import UserEntity
|
from src.application.domain.entities import UserEntity
|
||||||
from src.infrastructure.database.models import UserModel
|
from src.infrastructure.database.models import UserModel
|
||||||
@@ -27,7 +26,7 @@ class UserRepository(IUserRepository):
|
|||||||
user: UserModel | None = result.scalar_one_or_none()
|
user: UserModel | None = result.scalar_one_or_none()
|
||||||
if user is None:
|
if user is None:
|
||||||
self._logger.warning(f'User not found with user_id {user_id}')
|
self._logger.warning(f'User not found with user_id {user_id}')
|
||||||
raise ApplicationException(status_code=status.HTTP_404_NOT_FOUND, message='User not found')
|
raise NotFoundException(message='User not found')
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -60,7 +59,7 @@ class UserRepository(IUserRepository):
|
|||||||
raise
|
raise
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise ApplicationException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def _update_field(self, user_id: str, **fields: object) -> UserEntity:
|
async def _update_field(self, user_id: str, **fields: object) -> UserEntity:
|
||||||
try:
|
try:
|
||||||
@@ -74,7 +73,7 @@ class UserRepository(IUserRepository):
|
|||||||
raise
|
raise
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise ApplicationException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def set_phone(self, user_id: str, phone: str) -> UserEntity:
|
async def set_phone(self, user_id: str, phone: str) -> UserEntity:
|
||||||
return await self._update_field(user_id, phone=phone)
|
return await self._update_field(user_id, phone=phone)
|
||||||
@@ -83,10 +82,7 @@ class UserRepository(IUserRepository):
|
|||||||
allowed = {'passport_data', 'inn', 'erc20'}
|
allowed = {'passport_data', 'inn', 'erc20'}
|
||||||
payload = {k: v for k, v in fields.items() if k in allowed and v is not None}
|
payload = {k: v for k, v in fields.items() if k in allowed and v is not None}
|
||||||
if not payload:
|
if not payload:
|
||||||
raise ApplicationException(
|
raise BadRequestException(message='No identity fields to update')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
message='No identity fields to update',
|
|
||||||
)
|
|
||||||
return await self._update_field(user_id, **payload)
|
return await self._update_field(user_id, **payload)
|
||||||
|
|
||||||
async def set_encrypted_mnemonic(self, user_id: str, encrypted_mnemonic: str) -> UserEntity:
|
async def set_encrypted_mnemonic(self, user_id: str, encrypted_mnemonic: str) -> UserEntity:
|
||||||
@@ -100,7 +96,7 @@ class UserRepository(IUserRepository):
|
|||||||
raise
|
raise
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise ApplicationException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
||||||
return await self._update_field(user_id, password_hash=password_hash)
|
return await self._update_field(user_id, password_hash=password_hash)
|
||||||
@@ -121,4 +117,4 @@ class UserRepository(IUserRepository):
|
|||||||
return result.scalar_one_or_none() is not None
|
return result.scalar_one_or_none() is not None
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise ApplicationException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|||||||
18
src/main.py
18
src/main.py
@@ -2,18 +2,25 @@ from __future__ import annotations
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import secrets
|
import secrets
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from fastapi import Depends, FastAPI, status
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from starlette.exceptions import HTTPException
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from src.application.domain.exceptions import ApplicationException, UnauthorizedException
|
||||||
from src.infrastructure.cache import create_redis_client
|
from src.infrastructure.cache import create_redis_client
|
||||||
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
||||||
from src.infrastructure.utils import generate_instance_id
|
from src.infrastructure.utils import generate_instance_id
|
||||||
from src.infrastructure.logger import logger
|
from src.infrastructure.logger import logger
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.presentation.handlers import application_exception_handler, unhandled_exception_handler
|
from src.presentation.handlers import (
|
||||||
|
application_exception_handler,
|
||||||
|
http_exception_handler,
|
||||||
|
unhandled_exception_handler,
|
||||||
|
validation_exception_handler,
|
||||||
|
)
|
||||||
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
||||||
from src.presentation.routing import me_router
|
from src.presentation.routing import me_router
|
||||||
|
|
||||||
@@ -24,8 +31,7 @@ async def verify_credentials(credentials: HTTPBasicCredentials = Depends(securit
|
|||||||
user_ok = secrets.compare_digest(credentials.username, settings.DOCS_USERNAME)
|
user_ok = secrets.compare_digest(credentials.username, settings.DOCS_USERNAME)
|
||||||
pass_ok = secrets.compare_digest(credentials.password, settings.DOCS_PASSWORD)
|
pass_ok = secrets.compare_digest(credentials.password, settings.DOCS_PASSWORD)
|
||||||
if not (user_ok and pass_ok):
|
if not (user_ok and pass_ok):
|
||||||
raise ApplicationException(
|
raise UnauthorizedException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
message='Unauthorized',
|
message='Unauthorized',
|
||||||
headers={'WWW-Authenticate': 'Basic'},
|
headers={'WWW-Authenticate': 'Basic'},
|
||||||
)
|
)
|
||||||
@@ -78,6 +84,8 @@ app: FastAPI = FastAPI(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
||||||
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||||
app.add_exception_handler(ApplicationException, application_exception_handler)
|
app.add_exception_handler(ApplicationException, application_exception_handler)
|
||||||
app.add_exception_handler(Exception, unhandled_exception_handler)
|
app.add_exception_handler(Exception, unhandled_exception_handler)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import Depends, Request
|
from fastapi import Depends, Request
|
||||||
from fastapi.security.utils import get_authorization_scheme_param
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
from src.application.contracts import IJwtService
|
from src.application.contracts import IJwtService
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import UnauthorizedException
|
||||||
from src.application.domain.dto import AccessTokenPayload, AuthContext
|
from src.application.domain.dto import AccessTokenPayload, AuthContext
|
||||||
from src.presentation.dependencies import get_jwt_service
|
from src.presentation.dependencies import get_jwt_service
|
||||||
|
|
||||||
@@ -27,10 +27,10 @@ async def require_access_token(
|
|||||||
) -> AuthContext:
|
) -> AuthContext:
|
||||||
token = _extract_access_token(request)
|
token = _extract_access_token(request)
|
||||||
if not token:
|
if not token:
|
||||||
raise ApplicationException(status_code=401, message='Not authenticated')
|
raise UnauthorizedException(message='Not authenticated')
|
||||||
|
|
||||||
payload: AccessTokenPayload = await jwt_service.decode_access_token(token)
|
payload: AccessTokenPayload = await jwt_service.decode_access_token(token)
|
||||||
if payload.type != 'access':
|
if payload.type != 'access':
|
||||||
raise ApplicationException(status_code=401, message='Invalid token type')
|
raise UnauthorizedException(message='Invalid token type')
|
||||||
|
|
||||||
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)
|
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
from src.presentation.handlers.unhandled_handler import unhandled_exception_handler
|
from src.presentation.handlers.unhandled_handler import unhandled_exception_handler
|
||||||
from src.presentation.handlers.application_handler import application_exception_handler
|
from src.presentation.handlers.application_handler import application_exception_handler
|
||||||
|
from src.presentation.handlers.http_exception_handler import http_exception_handler
|
||||||
|
from src.presentation.handlers.validation_handler import validation_exception_handler
|
||||||
|
|||||||
11
src/presentation/handlers/http_exception_handler.py
Normal file
11
src/presentation/handlers/http_exception_handler.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
async def http_exception_handler(_request: Request, exc: HTTPException) -> ORJSONResponse:
|
||||||
|
return ORJSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={'detail': exc.detail},
|
||||||
|
headers=dict(exc.headers) if exc.headers else None,
|
||||||
|
)
|
||||||
10
src/presentation/handlers/validation_handler.py
Normal file
10
src/presentation/handlers/validation_handler.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
|
|
||||||
|
|
||||||
|
async def validation_exception_handler(_request: Request, exc: RequestValidationError) -> ORJSONResponse:
|
||||||
|
return ORJSONResponse(
|
||||||
|
status_code=422,
|
||||||
|
content={'detail': exc.errors()},
|
||||||
|
)
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from src.presentation.routing.account import account_router
|
from src.presentation.routing.account import account_router
|
||||||
|
from src.presentation.routing.account_settings import account_settings_router
|
||||||
|
|
||||||
|
|
||||||
me_router = APIRouter(prefix='/me', tags=['Account'])
|
me_router = APIRouter(prefix='/me', tags=['Account'])
|
||||||
|
|
||||||
me_router.include_router(account_router)
|
me_router.include_router(account_router)
|
||||||
|
me_router.include_router(account_settings_router)
|
||||||
# from src.presentation.routing.account_settings import account_settings_router
|
|
||||||
# me_router.include_router(account_settings_router)
|
|
||||||
|
|
||||||
# from src.presentation.routing.devices import devices_router
|
# from src.presentation.routing.devices import devices_router
|
||||||
# me_devices_router = APIRouter(prefix='/me', tags=['Devices'])
|
# me_devices_router = APIRouter(prefix='/me', tags=['Devices'])
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel, field_validator
|
||||||
from src.application.domain.exceptions import ApplicationException
|
|
||||||
|
|
||||||
|
|
||||||
class SetPhoneRequest(BaseModel):
|
class SetPhoneRequest(BaseModel):
|
||||||
@@ -12,6 +11,6 @@ class SetPhoneRequest(BaseModel):
|
|||||||
cleaned = re.sub(r'[\s\-\(\)]', '', v)
|
cleaned = re.sub(r'[\s\-\(\)]', '', v)
|
||||||
pattern = r'^(\+7|8)\d{10}$'
|
pattern = r'^(\+7|8)\d{10}$'
|
||||||
if not re.match(pattern, cleaned):
|
if not re.match(pattern, cleaned):
|
||||||
raise ApplicationException(message='Invalid Russian phone number', status_code=429)
|
raise ValueError('Invalid Russian phone number')
|
||||||
normalized = '+7' + cleaned[-10:]
|
normalized = '+7' + cleaned[-10:]
|
||||||
return normalized
|
return normalized
|
||||||
Reference in New Issue
Block a user