From fb7856260f1dfc2b65b83802f1e8f15d8e6aa9d6 Mon Sep 17 00:00:00 2001 From: dev1lfreak Date: Thu, 28 May 2026 14:13:43 +0300 Subject: [PATCH 1/5] refactor: divided exceptions into files --- src/application/domain/exceptions/__init__.py | 20 +++++----- .../exceptions/application_exceptions.py | 40 ------------------- .../exceptions/bad_request_exception.py | 8 ++++ .../domain/exceptions/conflict_exception.py | 8 ++++ .../domain/exceptions/forbidden_exception.py | 8 ++++ .../domain/exceptions/internal_exception.py | 8 ++++ .../domain/exceptions/not_found_exception.py | 8 ++++ .../service_unavailable_exception.py | 8 ++++ .../exceptions/too_many_requests_exception.py | 8 ++++ .../exceptions/unauthorized_exception.py | 8 ++++ 10 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 src/application/domain/exceptions/bad_request_exception.py create mode 100644 src/application/domain/exceptions/conflict_exception.py create mode 100644 src/application/domain/exceptions/forbidden_exception.py create mode 100644 src/application/domain/exceptions/internal_exception.py create mode 100644 src/application/domain/exceptions/not_found_exception.py create mode 100644 src/application/domain/exceptions/service_unavailable_exception.py create mode 100644 src/application/domain/exceptions/too_many_requests_exception.py create mode 100644 src/application/domain/exceptions/unauthorized_exception.py diff --git a/src/application/domain/exceptions/__init__.py b/src/application/domain/exceptions/__init__.py index 03368ee..e882cff 100644 --- a/src/application/domain/exceptions/__init__.py +++ b/src/application/domain/exceptions/__init__.py @@ -1,11 +1,9 @@ -from src.application.domain.exceptions.application_exceptions import ( - ApplicationException, - BadRequestException, - ConflictException, - ForbiddenException, - InternalException, - NotFoundException, - ServiceUnavailableException, - TooManyRequestsException, - UnauthorizedException, -) +from src.application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.bad_request_exception import BadRequestException +from src.application.domain.exceptions.unauthorized_exception import UnauthorizedException +from src.application.domain.exceptions.forbidden_exception import ForbiddenException +from src.application.domain.exceptions.not_found_exception import NotFoundException +from src.application.domain.exceptions.conflict_exception import ConflictException +from src.application.domain.exceptions.internal_exception import InternalException +from src.application.domain.exceptions.service_unavailable_exception import ServiceUnavailableException +from src.application.domain.exceptions.too_many_requests_exception import TooManyRequestsException diff --git a/src/application/domain/exceptions/application_exceptions.py b/src/application/domain/exceptions/application_exceptions.py index 03cecd3..4892289 100644 --- a/src/application/domain/exceptions/application_exceptions.py +++ b/src/application/domain/exceptions/application_exceptions.py @@ -17,43 +17,3 @@ class ApplicationException(Exception): def __str__(self) -> str: 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) diff --git a/src/application/domain/exceptions/bad_request_exception.py b/src/application/domain/exceptions/bad_request_exception.py new file mode 100644 index 0000000..a55bcec --- /dev/null +++ b/src/application/domain/exceptions/bad_request_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class BadRequestException(ApplicationException): + def __init__(self, message: str, headers: Mapping[str, str] | None = None): + super().__init__(400, message, headers) diff --git a/src/application/domain/exceptions/conflict_exception.py b/src/application/domain/exceptions/conflict_exception.py new file mode 100644 index 0000000..462dfc7 --- /dev/null +++ b/src/application/domain/exceptions/conflict_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class ConflictException(ApplicationException): + def __init__(self, message: str, headers: Mapping[str, str] | None = None): + super().__init__(409, message, headers) diff --git a/src/application/domain/exceptions/forbidden_exception.py b/src/application/domain/exceptions/forbidden_exception.py new file mode 100644 index 0000000..fb561c4 --- /dev/null +++ b/src/application/domain/exceptions/forbidden_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class ForbiddenException(ApplicationException): + def __init__(self, message: str = 'Forbidden', headers: Mapping[str, str] | None = None): + super().__init__(403, message, headers) diff --git a/src/application/domain/exceptions/internal_exception.py b/src/application/domain/exceptions/internal_exception.py new file mode 100644 index 0000000..6b34899 --- /dev/null +++ b/src/application/domain/exceptions/internal_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class InternalException(ApplicationException): + def __init__(self, message: str = 'Internal Server Error', headers: Mapping[str, str] | None = None): + super().__init__(500, message, headers) diff --git a/src/application/domain/exceptions/not_found_exception.py b/src/application/domain/exceptions/not_found_exception.py new file mode 100644 index 0000000..2fb4a43 --- /dev/null +++ b/src/application/domain/exceptions/not_found_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class NotFoundException(ApplicationException): + def __init__(self, message: str = 'Not found', headers: Mapping[str, str] | None = None): + super().__init__(404, message, headers) diff --git a/src/application/domain/exceptions/service_unavailable_exception.py b/src/application/domain/exceptions/service_unavailable_exception.py new file mode 100644 index 0000000..8d6c7ab --- /dev/null +++ b/src/application/domain/exceptions/service_unavailable_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class ServiceUnavailableException(ApplicationException): + def __init__(self, message: str, headers: Mapping[str, str] | None = None): + super().__init__(503, message, headers) diff --git a/src/application/domain/exceptions/too_many_requests_exception.py b/src/application/domain/exceptions/too_many_requests_exception.py new file mode 100644 index 0000000..adf2a16 --- /dev/null +++ b/src/application/domain/exceptions/too_many_requests_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class TooManyRequestsException(ApplicationException): + def __init__(self, message: str, headers: Mapping[str, str] | None = None): + super().__init__(429, message, headers) diff --git a/src/application/domain/exceptions/unauthorized_exception.py b/src/application/domain/exceptions/unauthorized_exception.py new file mode 100644 index 0000000..a92fd35 --- /dev/null +++ b/src/application/domain/exceptions/unauthorized_exception.py @@ -0,0 +1,8 @@ +from application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class UnauthorizedException(ApplicationException): + def __init__(self, message: str = 'Unauthorized', headers: Mapping[str, str] | None = None): + super().__init__(401, message, headers) -- 2.49.1 From b9e980db9473fd4d1c3d26f6617bf31bb53c51b7 Mon Sep 17 00:00:00 2001 From: dev1lfreak Date: Thu, 28 May 2026 16:56:07 +0300 Subject: [PATCH 2/5] refactor: change import paths --- src/application/domain/exceptions/bad_request_exception.py | 2 +- src/application/domain/exceptions/conflict_exception.py | 2 +- src/application/domain/exceptions/forbidden_exception.py | 2 +- src/application/domain/exceptions/internal_exception.py | 2 +- src/application/domain/exceptions/not_found_exception.py | 2 +- .../domain/exceptions/service_unavailable_exception.py | 2 +- .../domain/exceptions/too_many_requests_exception.py | 2 +- src/application/domain/exceptions/unauthorized_exception.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/application/domain/exceptions/bad_request_exception.py b/src/application/domain/exceptions/bad_request_exception.py index a55bcec..614285c 100644 --- a/src/application/domain/exceptions/bad_request_exception.py +++ b/src/application/domain/exceptions/bad_request_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/conflict_exception.py b/src/application/domain/exceptions/conflict_exception.py index 462dfc7..b91af99 100644 --- a/src/application/domain/exceptions/conflict_exception.py +++ b/src/application/domain/exceptions/conflict_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/forbidden_exception.py b/src/application/domain/exceptions/forbidden_exception.py index fb561c4..18809d7 100644 --- a/src/application/domain/exceptions/forbidden_exception.py +++ b/src/application/domain/exceptions/forbidden_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/internal_exception.py b/src/application/domain/exceptions/internal_exception.py index 6b34899..b3c31ae 100644 --- a/src/application/domain/exceptions/internal_exception.py +++ b/src/application/domain/exceptions/internal_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/not_found_exception.py b/src/application/domain/exceptions/not_found_exception.py index 2fb4a43..33a7056 100644 --- a/src/application/domain/exceptions/not_found_exception.py +++ b/src/application/domain/exceptions/not_found_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/service_unavailable_exception.py b/src/application/domain/exceptions/service_unavailable_exception.py index 8d6c7ab..db05f65 100644 --- a/src/application/domain/exceptions/service_unavailable_exception.py +++ b/src/application/domain/exceptions/service_unavailable_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/too_many_requests_exception.py b/src/application/domain/exceptions/too_many_requests_exception.py index adf2a16..64c47fa 100644 --- a/src/application/domain/exceptions/too_many_requests_exception.py +++ b/src/application/domain/exceptions/too_many_requests_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping diff --git a/src/application/domain/exceptions/unauthorized_exception.py b/src/application/domain/exceptions/unauthorized_exception.py index a92fd35..bd103a5 100644 --- a/src/application/domain/exceptions/unauthorized_exception.py +++ b/src/application/domain/exceptions/unauthorized_exception.py @@ -1,4 +1,4 @@ -from application.domain.exceptions.application_exceptions import ApplicationException +from src.application.domain.exceptions.application_exceptions import ApplicationException from typing import Mapping -- 2.49.1 From 48e917eecebb3bc39e6a6734f8261a66cf2a3997 Mon Sep 17 00:00:00 2001 From: dev1lfreak Date: Thu, 28 May 2026 18:23:44 +0300 Subject: [PATCH 3/5] refactor: change exceptions to more specific --- .../commands/change_email_complete.py | 10 +++---- .../commands/change_email_confirm_old.py | 20 +++++++------- .../commands/change_email_start.py | 14 +++++----- .../commands/change_password_complete.py | 14 +++++----- .../commands/change_password_start.py | 14 +++++----- .../commands/forgot_password_complete.py | 14 +++++----- .../commands/forgot_password_start.py | 12 ++++----- .../set_encrypted_mnemonic_complete.py | 12 ++++----- .../commands/set_encrypted_mnemonic_start.py | 16 +++++------ .../commands/update_bank_details_complete.py | 10 +++---- .../commands/update_bank_details_start.py | 14 +++++----- src/infrastructure/security/csrf.py | 27 +++++-------------- src/infrastructure/security/jwt.py | 22 +++++++-------- src/infrastructure/vault/keys.py | 4 +-- src/presentation/decorators/csrf.py | 7 ++--- src/presentation/decorators/rate_limit.py | 13 +++++---- 16 files changed, 104 insertions(+), 119 deletions(-) diff --git a/src/application/commands/change_email_complete.py b/src/application/commands/change_email_complete.py index 0ccfbf7..34c3d8d 100644 --- a/src/application/commands/change_email_complete.py +++ b/src/application/commands/change_email_complete.py @@ -1,6 +1,6 @@ from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ILogger, ICache -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import BadRequestException from src.infrastructure.database.decorators import transactional @@ -30,16 +30,16 @@ class ChangeEmailCompleteCommand: cached_user_id = await self._cache.get(new_code_key) if not cached_user_id: self._logger.info(f'Change email complete failed: code not found (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') if cached_user_id != user_id: self._logger.info(f'Change email complete failed: code-user mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') raw_value = await self._cache.get(new_user_key) if not raw_value: self._logger.info(f'Change email complete failed: user key missing (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') separator_idx = raw_value.index(':') code_hash = raw_value[:separator_idx] @@ -48,7 +48,7 @@ class ChangeEmailCompleteCommand: ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code) if not ok: self._logger.info(f'Change email complete failed: code hash mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') user = await self._unit_of_work.user_repository.set_email(user_id=user_id, email=new_email) await self._cache.set_user(user_id, user) diff --git a/src/application/commands/change_email_confirm_old.py b/src/application/commands/change_email_confirm_old.py index 95d955f..12643f9 100644 --- a/src/application/commands/change_email_confirm_old.py +++ b/src/application/commands/change_email_confirm_old.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import BadRequestException, ConflictException, ServiceUnavailableException from src.infrastructure.config import settings from src.infrastructure.context_vars import trace_id_var from src.infrastructure.database.decorators import transactional @@ -41,32 +41,32 @@ class ChangeEmailConfirmOldCommand: cached_user_id = await self._cache.get(old_code_key) if not cached_user_id: self._logger.info(f'Change email confirm-old failed: code not found (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') if cached_user_id != user_id: self._logger.info(f'Change email confirm-old failed: code-user mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') code_hash = await self._cache.get(old_user_key) if not code_hash: self._logger.info(f'Change email confirm-old failed: user key missing (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code) if not ok: self._logger.info(f'Change email confirm-old failed: code hash mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id) if user.email and user.email.lower() == new_email.lower(): self._logger.info(f'Change email confirm-old failed: new email same as current (user_id={user_id})') - raise ApplicationException(400, 'New email must differ from the current one') + raise BadRequestException(message='New email must differ from the current one') email_taken = await self._unit_of_work.user_repository.email_exists(email=new_email) if email_taken: self._logger.info(f'Change email confirm-old failed: new email already taken (user_id={user_id})') - raise ApplicationException(409, 'Email already in use') + raise ConflictException(message='Email already in use') try: await self._cache.delete(old_code_key) @@ -94,7 +94,7 @@ class ChangeEmailConfirmOldCommand: if not saved: await self._cache.delete(new_code_key) self._logger.error(f'Change email confirm-old failed: cannot save new code hash for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') message_id = str(ULID()) now = datetime.now(timezone.utc).isoformat() @@ -137,9 +137,9 @@ class ChangeEmailConfirmOldCommand: self._logger.error(f'Publish failed and rollback cache failed for user_id={user_id}: {str(rollback_err)}') self._logger.error(f'Failed to publish change email new code for user_id={user_id}: {str(exception)}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') return True self._logger.error(f'Change email confirm-old failed: code space exhausted for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') diff --git a/src/application/commands/change_email_start.py b/src/application/commands/change_email_start.py index e9047df..f9660dc 100644 --- a/src/application/commands/change_email_start.py +++ b/src/application/commands/change_email_start.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import NotFoundException, TooManyRequestsException, ServiceUnavailableException from src.infrastructure.config import settings from src.infrastructure.context_vars import trace_id_var from src.infrastructure.database.decorators import transactional @@ -38,7 +38,7 @@ class ChangeEmailStartCommand: if not user.email: self._logger.warning(f'User {user_id} does not have an email address') - raise ApplicationException(404, f'User {user_id} does not have an email address') + raise NotFoundException(message=f'User {user_id} does not have an email address') trace_id = trace_id_var.get() if not trace_id or trace_id == 'N/A': @@ -48,7 +48,7 @@ class ChangeEmailStartCommand: locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL) if not locked: self._logger.info(f'Change email throttled by lock (user_id={user_id})') - raise ApplicationException(429, 'Too many requests. Please wait.') + raise TooManyRequestsException(message='Too many requests. Please wait.') try: user_key = f'{USER_PREFIX}{user_id}' @@ -56,7 +56,7 @@ class ChangeEmailStartCommand: existing = await self._cache.get(user_key) if existing: self._logger.info(f'Change email denied: code already exists for user_id={user_id}') - raise ApplicationException(429, 'Code already sent. Please wait before retrying.') + raise TooManyRequestsException(message='Code already sent. Please wait before retrying.') for _ in range(MAX_ATTEMPTS): code = f'{secrets.randbelow(1_000_000):06d}' @@ -72,7 +72,7 @@ class ChangeEmailStartCommand: if not saved: await self._cache.delete(code_key) self._logger.error(f'Change email failed: cannot save code hash for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') message_id = str(ULID()) now = datetime.now(timezone.utc).isoformat() @@ -115,12 +115,12 @@ class ChangeEmailStartCommand: self._logger.error(f'Publish failed and rollback cache failed for user_id={user_id}: {str(rollback_err)}') self._logger.error(f'Failed to publish change email old code for user_id={user_id}: {str(exception)}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') return True self._logger.error(f'Change email failed: code space exhausted for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') finally: await self._cache.delete(lock_key) diff --git a/src/application/commands/change_password_complete.py b/src/application/commands/change_password_complete.py index 9dc7367..0fd1ab3 100644 --- a/src/application/commands/change_password_complete.py +++ b/src/application/commands/change_password_complete.py @@ -1,6 +1,6 @@ from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ILogger, ICache -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import BadRequestException from src.infrastructure.database.decorators import transactional @@ -36,33 +36,33 @@ class ChangePasswordCompleteCommand: if new_password != confirm_password: self._logger.info(f'Change password failed: passwords do not match (user_id={user_id})') - raise ApplicationException(400, 'Passwords do not match') + raise BadRequestException(message='Passwords do not match') cached_user_id = await self._cache.get(code_key) if not cached_user_id: self._logger.info(f'Change password failed: code not found (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') if cached_user_id != user_id: self._logger.info(f'Change password failed: code-user mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') code_hash = await self._cache.get(user_key) if not code_hash: self._logger.info(f'Change password failed: user key missing (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code) if not ok: self._logger.info(f'Change password failed: code hash mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') current_password_hash = await self._unit_of_work.user_repository.get_password_hash(user_id=user_id) is_same = await self._hash_service.verify(hashed_value=current_password_hash, plain_value=new_password) if is_same: self._logger.info(f'Change password failed: new password same as current (user_id={user_id})') - raise ApplicationException(400, 'New password must differ from the current one') + raise BadRequestException(message='New password must differ from the current one') new_password_hash = await self._hash_service.hash(new_password) user = await self._unit_of_work.user_repository.set_password( diff --git a/src/application/commands/change_password_start.py b/src/application/commands/change_password_start.py index d3c1138..8667e63 100644 --- a/src/application/commands/change_password_start.py +++ b/src/application/commands/change_password_start.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import NotFoundException, TooManyRequestsException, ServiceUnavailableException from src.infrastructure.config import settings from src.infrastructure.context_vars import trace_id_var from src.infrastructure.database.decorators import transactional @@ -38,7 +38,7 @@ class ChangePasswordStartCommand: if not user.email: self._logger.warning(f'User {user_id} does not have an email address') - raise ApplicationException(404, f'User {user_id} does not have an email address') + raise NotFoundException(message=f'User {user_id} does not have an email address') trace_id = trace_id_var.get() if not trace_id or trace_id == 'N/A': @@ -48,7 +48,7 @@ class ChangePasswordStartCommand: locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL) if not locked: self._logger.info(f'Change password throttled by lock (user_id={user_id})') - raise ApplicationException(429, 'Too many requests. Please wait.') + raise TooManyRequestsException(message='Too many requests. Please wait.') try: user_key = f'{USER_PREFIX}{user_id}' @@ -56,7 +56,7 @@ class ChangePasswordStartCommand: existing = await self._cache.get(user_key) if existing: self._logger.info(f'Change password denied: code already exists for user_id={user_id}') - raise ApplicationException(429, 'Code already sent. Please wait before retrying.') + raise TooManyRequestsException(message='Code already sent. Please wait before retrying.') for _ in range(MAX_ATTEMPTS): code = f'{secrets.randbelow(1_000_000):06d}' @@ -72,7 +72,7 @@ class ChangePasswordStartCommand: if not saved: await self._cache.delete(code_key) self._logger.error(f'Change password failed: cannot save code hash for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') message_id = str(ULID()) now = datetime.now(timezone.utc).isoformat() @@ -115,12 +115,12 @@ class ChangePasswordStartCommand: self._logger.error(f'Publish failed and rollback cache failed for user_id={user_id}: {str(rollback_err)}') self._logger.error(f'Failed to publish change password email for user_id={user_id}: {str(exception)}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') return True self._logger.error(f'Change password failed: code space exhausted for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') finally: await self._cache.delete(lock_key) diff --git a/src/application/commands/forgot_password_complete.py b/src/application/commands/forgot_password_complete.py index e06f49f..44689d9 100644 --- a/src/application/commands/forgot_password_complete.py +++ b/src/application/commands/forgot_password_complete.py @@ -1,6 +1,6 @@ from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ILogger, ICache -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import BadRequestException from src.infrastructure.database.decorators import transactional @@ -38,33 +38,33 @@ class ForgotPasswordCompleteCommand: if new_password != confirm_password: self._logger.info('Forgot password failed: passwords do not match') - raise ApplicationException(400, 'Passwords do not match') + raise BadRequestException(message='Passwords do not match') code_key = f'{CODE_PREFIX}{code}' cached_email = await self._cache.get(code_key) if not cached_email: self._logger.info('Forgot password failed: code not found') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') if cached_email != normalized: self._logger.info('Forgot password failed: code-email mismatch') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') email_key = f'{EMAIL_PREFIX}{normalized}' code_hash = await self._cache.get(email_key) if not code_hash: self._logger.info('Forgot password failed: email key missing') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code) if not ok: self._logger.info('Forgot password failed: code hash mismatch') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') user = await self._unit_of_work.user_repository.get_user_by_email(normalized) if user is None: self._logger.info('Forgot password failed: user not found after valid code') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') new_password_hash = await self._hash_service.hash(new_password) user = await self._unit_of_work.user_repository.set_password( diff --git a/src/application/commands/forgot_password_start.py b/src/application/commands/forgot_password_start.py index da7f86d..a0ca1a4 100644 --- a/src/application/commands/forgot_password_start.py +++ b/src/application/commands/forgot_password_start.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import TooManyRequestsException, ServiceUnavailableException from src.infrastructure.config import settings from src.infrastructure.context_vars import trace_id_var from src.infrastructure.database.decorators import transactional @@ -52,7 +52,7 @@ class ForgotPasswordStartCommand: locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL) if not locked: self._logger.info(f'Forgot password throttled by lock (user_id={user.id})') - raise ApplicationException(429, 'Too many requests. Please wait.') + raise TooManyRequestsException(message='Too many requests. Please wait.') try: email_key = f'{EMAIL_PREFIX}{normalized}' @@ -60,7 +60,7 @@ class ForgotPasswordStartCommand: existing = await self._cache.get(email_key) if existing: self._logger.info(f'Forgot password denied: code already exists for user_id={user.id}') - raise ApplicationException(429, 'Code already sent. Please wait before retrying.') + raise TooManyRequestsException(message='Code already sent. Please wait before retrying.') for _ in range(MAX_ATTEMPTS): code = f'{secrets.randbelow(1_000_000):06d}' @@ -76,7 +76,7 @@ class ForgotPasswordStartCommand: if not saved: await self._cache.delete(code_key) self._logger.error(f'Forgot password failed: cannot save code hash for user_id={user.id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') message_id = str(ULID()) now = datetime.now(timezone.utc).isoformat() @@ -121,12 +121,12 @@ class ForgotPasswordStartCommand: ) self._logger.error(f'Failed to publish forgot password email for user_id={user.id}: {str(exception)}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') return True self._logger.error(f'Forgot password failed: code space exhausted for user_id={user.id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') finally: await self._cache.delete(lock_key) diff --git a/src/application/commands/set_encrypted_mnemonic_complete.py b/src/application/commands/set_encrypted_mnemonic_complete.py index 0112421..6a5635d 100644 --- a/src/application/commands/set_encrypted_mnemonic_complete.py +++ b/src/application/commands/set_encrypted_mnemonic_complete.py @@ -1,7 +1,7 @@ from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ILogger, ICache from src.application.domain.entities import UserEntity -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import BadRequestException, ConflictException from src.infrastructure.database.decorators import transactional @@ -31,26 +31,26 @@ class SetEncryptedMnemonicCompleteCommand: user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id) if user.encrypted_mnemonic is not None: self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}') - raise ApplicationException(409, 'Encrypted mnemonic already set and cannot be changed') + raise ConflictException(message='Encrypted mnemonic already set and cannot be changed') cached_user_id = await self._cache.get(code_key) if not cached_user_id: self._logger.info(f'Encrypted mnemonic set failed: code not found (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') if cached_user_id != user_id: self._logger.info(f'Encrypted mnemonic set failed: code-user mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') code_hash = await self._cache.get(user_key) if not code_hash: self._logger.info(f'Encrypted mnemonic set failed: user key missing (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code) if not ok: self._logger.info(f'Encrypted mnemonic set failed: code hash mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') user = await self._unit_of_work.user_repository.set_encrypted_mnemonic( user_id=user_id, diff --git a/src/application/commands/set_encrypted_mnemonic_start.py b/src/application/commands/set_encrypted_mnemonic_start.py index 2b115fe..68edbb4 100644 --- a/src/application/commands/set_encrypted_mnemonic_start.py +++ b/src/application/commands/set_encrypted_mnemonic_start.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import ConflictException, NotFoundException, TooManyRequestsException, ServiceUnavailableException from src.infrastructure.config import settings from src.infrastructure.context_vars import trace_id_var from src.infrastructure.database.decorators import transactional @@ -38,11 +38,11 @@ class SetEncryptedMnemonicStartCommand: if user.encrypted_mnemonic is not None: self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}') - raise ApplicationException(409, 'Encrypted mnemonic already set and cannot be changed') + raise ConflictException(message='Encrypted mnemonic already set and cannot be changed') if not user.email: self._logger.warning(f'User {user_id} does not have an email address') - raise ApplicationException(404, f'User {user_id} does not have an email address') + raise NotFoundException(message=f'User {user_id} does not have an email address') trace_id = trace_id_var.get() if not trace_id or trace_id == 'N/A': @@ -52,7 +52,7 @@ class SetEncryptedMnemonicStartCommand: locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL) if not locked: self._logger.info(f'Encrypted mnemonic set throttled by lock (user_id={user_id})') - raise ApplicationException(429, 'Too many requests. Please wait.') + raise TooManyRequestsException(message='Too many requests. Please wait.') try: user_key = f'{USER_PREFIX}{user_id}' @@ -60,7 +60,7 @@ class SetEncryptedMnemonicStartCommand: existing = await self._cache.get(user_key) if existing: self._logger.info(f'Encrypted mnemonic set denied: code already exists for user_id={user_id}') - raise ApplicationException(429, 'Code already sent. Please wait before retrying.') + raise TooManyRequestsException(message='Code already sent. Please wait before retrying.') for _ in range(MAX_ATTEMPTS): code = f'{secrets.randbelow(1_000_000):06d}' @@ -76,7 +76,7 @@ class SetEncryptedMnemonicStartCommand: if not saved: await self._cache.delete(code_key) self._logger.error(f'Encrypted mnemonic set failed: cannot save code hash for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') message_id = str(ULID()) now = datetime.now(timezone.utc).isoformat() @@ -119,12 +119,12 @@ class SetEncryptedMnemonicStartCommand: self._logger.error(f'Publish failed and rollback cache failed for user_id={user_id}: {str(rollback_err)}') self._logger.error(f'Failed to publish encrypted mnemonic set email for user_id={user_id}: {str(exception)}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') return True self._logger.error(f'Encrypted mnemonic set failed: code space exhausted for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') finally: await self._cache.delete(lock_key) diff --git a/src/application/commands/update_bank_details_complete.py b/src/application/commands/update_bank_details_complete.py index 1c5ba8a..fd08004 100644 --- a/src/application/commands/update_bank_details_complete.py +++ b/src/application/commands/update_bank_details_complete.py @@ -1,7 +1,7 @@ from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ILogger, ICache from src.application.domain.entities import UserEntity -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import BadRequestException from src.infrastructure.database.decorators import transactional @@ -39,21 +39,21 @@ class UpdateBankDetailsCompleteCommand: cached_user_id = await self._cache.get(code_key) if not cached_user_id: self._logger.info(f'Bank details update failed: code not found (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') if cached_user_id != user_id: self._logger.info(f'Bank details update failed: code-user mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') code_hash = await self._cache.get(user_key) if not code_hash: self._logger.info(f'Bank details update failed: user key missing (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code) if not ok: self._logger.info(f'Bank details update failed: code hash mismatch (user_id={user_id})') - raise ApplicationException(400, 'Invalid or expired code') + raise BadRequestException(message='Invalid or expired code') fields = {} if passport_data is not None: diff --git a/src/application/commands/update_bank_details_start.py b/src/application/commands/update_bank_details_start.py index f3d8148..9506975 100644 --- a/src/application/commands/update_bank_details_start.py +++ b/src/application/commands/update_bank_details_start.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import NotFoundException, TooManyRequestsException, ServiceUnavailableException from src.infrastructure.config import settings from src.infrastructure.context_vars import trace_id_var from src.infrastructure.database.decorators import transactional @@ -38,7 +38,7 @@ class UpdateBankDetailsStartCommand: if not user.email: self._logger.warning(f'User {user_id} does not have an email address') - raise ApplicationException(status_code=404, message=f'User {user_id} does not have an email address') + raise NotFoundException(message=f'User {user_id} does not have an email address') trace_id = trace_id_var.get() if not trace_id or trace_id == 'N/A': @@ -48,7 +48,7 @@ class UpdateBankDetailsStartCommand: locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL) if not locked: self._logger.info(f'Bank details update throttled by lock (user_id={user_id})') - raise ApplicationException(429, 'Too many requests. Please wait.') + raise TooManyRequestsException(message='Too many requests. Please wait.') try: user_key = f'{USER_PREFIX}{user_id}' @@ -56,7 +56,7 @@ class UpdateBankDetailsStartCommand: existing = await self._cache.get(user_key) if existing: self._logger.info(f'Bank details update denied: code already exists for user_id={user_id}') - raise ApplicationException(429, 'Code already sent. Please wait before retrying.') + raise TooManyRequestsException(message='Code already sent. Please wait before retrying.') for _ in range(MAX_ATTEMPTS): code = f'{secrets.randbelow(1_000_000):06d}' @@ -72,7 +72,7 @@ class UpdateBankDetailsStartCommand: if not saved: await self._cache.delete(code_key) self._logger.error(f'Bank details update failed: cannot save code hash for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') message_id = str(ULID()) now = datetime.now(timezone.utc).isoformat() @@ -115,12 +115,12 @@ class UpdateBankDetailsStartCommand: self._logger.error(f'Publish failed and rollback cache failed for user_id={user_id}: {str(rollback_err)}') self._logger.error(f'Failed to publish bank details update email for user_id={user_id}: {str(exception)}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') return True self._logger.error(f'Bank details update failed: code space exhausted for user_id={user_id}') - raise ApplicationException(503, 'Temporary error. Please try again.') + raise ServiceUnavailableException(message='Temporary error. Please try again.') finally: await self._cache.delete(lock_key) diff --git a/src/infrastructure/security/csrf.py b/src/infrastructure/security/csrf.py index 1b6d3fd..0d202ce 100644 --- a/src/infrastructure/security/csrf.py +++ b/src/infrastructure/security/csrf.py @@ -3,7 +3,7 @@ import secrets from typing import Any, Optional, Mapping from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature from src.application.contracts import ICsrfService -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import ForbiddenException from src.infrastructure.config.settings import settings @@ -42,21 +42,12 @@ class CsrfService(ICsrfService): try: data = self._serializer.loads(token, max_age=self.TTL_SECONDS) except SignatureExpired: - raise ApplicationException( - status_code=403, - message='CSRF token expired', - ) + raise ForbiddenException(message='CSRF token expired') except BadSignature: - raise ApplicationException( - status_code=403, - message='CSRF token invalid', - ) + raise ForbiddenException(message='CSRF token invalid') if expected_subject is not None and data.get('sub') != expected_subject: - raise ApplicationException( - status_code=403, - message='CSRF token subject mismatch', - ) + raise ForbiddenException(message='CSRF token subject mismatch') return data @@ -67,15 +58,9 @@ class CsrfService(ICsrfService): def verify_pair(self, cookie_token: Optional[str], header_token: Optional[str], expected_subject: Optional[str] = None) -> None: if not cookie_token or not header_token: - raise ApplicationException( - status_code=403, - message='CSRF token missing', - ) + raise ForbiddenException(message='CSRF token missing') if not secrets.compare_digest(cookie_token, header_token): - raise ApplicationException( - status_code=403, - message='CSRF token mismatch', - ) + raise ForbiddenException(message='CSRF token mismatch') self.verify(cookie_token, expected_subject=expected_subject) diff --git a/src/infrastructure/security/jwt.py b/src/infrastructure/security/jwt.py index 4274902..7102708 100644 --- a/src/infrastructure/security/jwt.py +++ b/src/infrastructure/security/jwt.py @@ -2,7 +2,7 @@ from __future__ import annotations from jose import jwt, ExpiredSignatureError, JWTError from src.application.contracts import ILogger, IJwtService from src.application.domain.dto import AccessTokenPayload -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import ApplicationException, UnauthorizedException, InternalException from src.infrastructure.config.settings import settings from src.infrastructure.vault import JwtKeyStore @@ -17,7 +17,7 @@ class JwtService(IJwtService): if payload.get('type') != 'access': self._logger.warning(f'Access token invalid type received_type={payload.get('type')}') - raise ApplicationException(status_code=401, message='Invalid token type') + raise UnauthorizedException(message='Invalid token type') try: return AccessTokenPayload( @@ -32,7 +32,7 @@ class JwtService(IJwtService): ) except KeyError as exception: self._logger.warning(f'Access token missing claim error={str(exception)}') - raise ApplicationException(status_code=401, message=f'Missing token claim: {exception}') + raise UnauthorizedException(message=f'Missing token claim: {exception}') async def _decode_and_verify(self, token: str) -> dict: kid: str | None = None @@ -42,12 +42,12 @@ class JwtService(IJwtService): kid = header.get('kid') if not kid: self._logger.warning(f'JWT header missing kid header={header}') - raise ApplicationException(status_code=401, message='Missing token header: kid') + raise UnauthorizedException(message='Missing token header: kid') received_alg = header.get('alg') if received_alg != settings.JWT_ALGORITHM: self._logger.warning(f'JWT invalid algorithm kid={kid} received_alg={received_alg} expected_alg={settings.JWT_ALGORITHM}') - raise ApplicationException(status_code=401, message='Invalid token algorithm') + raise UnauthorizedException(message='Invalid token algorithm') public_pem = await self._key_store.get_public_key_for_kid(str(kid)) @@ -58,7 +58,7 @@ class JwtService(IJwtService): if not public_pem: self._logger.warning(f'JWT unknown kid kid={kid}') - raise ApplicationException(status_code=401, message='Unknown token kid') + raise UnauthorizedException(message='Unknown token kid') options = { 'verify_signature': True, @@ -85,25 +85,25 @@ class JwtService(IJwtService): if 'sid' not in payload: self._logger.warning(f'JWT missing sid claim kid={kid}') - raise ApplicationException(status_code=401, message='Missing token claim: sid') + raise UnauthorizedException(message='Missing token claim: sid') if 'type' not in payload: self._logger.warning(f'JWT missing type claim kid={kid}') - raise ApplicationException(status_code=401, message='Missing token claim: type') + raise UnauthorizedException(message='Missing token claim: type') return payload except ExpiredSignatureError as exception: self._logger.info(f'JWT expired kid={kid} error={str(exception)}') - raise ApplicationException(status_code=401, message='Token expired') + raise UnauthorizedException(message='Token expired') except ApplicationException: raise except JWTError as exception: self._logger.warning(f'JWT decode failed kid={kid} error={str(exception)}') - raise ApplicationException(status_code=401, message='Invalid token') + raise UnauthorizedException(message='Invalid token') except Exception as exception: self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}') - raise ApplicationException(status_code=500, message='JWT decode failed') \ No newline at end of file + raise InternalException(message='JWT decode failed') \ No newline at end of file diff --git a/src/infrastructure/vault/keys.py b/src/infrastructure/vault/keys.py index 473f580..9da61da 100644 --- a/src/infrastructure/vault/keys.py +++ b/src/infrastructure/vault/keys.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timezone from src.application.domain.dto import JwtPublicKeySet, JwtPublicKey -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import InternalException from src.infrastructure.vault.client import VaultClient @@ -52,7 +52,7 @@ class JwtKeyStore: @classmethod def get_instance(cls) -> 'JwtKeyStore': if cls._instance is None: - raise ApplicationException(status_code=500, message='JwtKeyStore not initialized') + raise InternalException(message='JwtKeyStore not initialized') return cls._instance def _read_keyset_sync(self) -> JwtPublicKeySet: diff --git a/src/presentation/decorators/csrf.py b/src/presentation/decorators/csrf.py index 768e69e..bc08e31 100644 --- a/src/presentation/decorators/csrf.py +++ b/src/presentation/decorators/csrf.py @@ -3,7 +3,7 @@ import inspect from functools import wraps from typing import Callable, Awaitable, Any, Optional, Annotated from fastapi import Request, Header -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import InternalException from src.infrastructure.security import CsrfService @@ -39,10 +39,7 @@ def csrf_protect( break if request is None: - raise ApplicationException( - status_code=500, - message='Request is required for CSRF protection', - ) + raise InternalException(message='Request is required for CSRF protection') csrf = CsrfService() diff --git a/src/presentation/decorators/rate_limit.py b/src/presentation/decorators/rate_limit.py index 6ff0094..964552b 100644 --- a/src/presentation/decorators/rate_limit.py +++ b/src/presentation/decorators/rate_limit.py @@ -6,7 +6,11 @@ from typing import Any, Awaitable, Callable, Literal, Optional, Protocol, runtim from fastapi import Request from redis.asyncio.client import Redis from src.application.contracts import ILogger -from src.application.domain.exceptions import ApplicationException +from src.application.domain.exceptions import ( + InternalException, + ServiceUnavailableException, + TooManyRequestsException, +) from src.infrastructure.logger import get_logger from src.presentation.dependencies import get_redis @@ -124,7 +128,7 @@ def rate_limit( ident = _call_key_builder(key_builder, request, args, kwargs) # type: ignore[arg-type] except Exception as e: logger.error(f'RateLimit key_builder failed error={str(e)}') - raise ApplicationException(500, 'Rate limiter key_builder failed') + raise InternalException(message='Rate limiter key_builder failed') route = request.url.path method = request.method @@ -153,13 +157,12 @@ def rate_limit( logger.warning(f'RateLimit fail-open activated key={redis_key}') return await func(*args, **kwargs) - raise ApplicationException(503, 'Rate limiter unavailable') + raise ServiceUnavailableException(message='Rate limiter unavailable') if count > limit: retry_after = max(ttl, 0) logger.warning(f'RateLimit exceeded key={redis_key} count={count} limit={limit} retry_after={retry_after}') - raise ApplicationException( - status_code=429, + raise TooManyRequestsException( message='Too Many Requests', headers={'Retry-After': str(retry_after)}, ) -- 2.49.1 From a1b41e831763fb473366acf2018c127b230bbb18 Mon Sep 17 00:00:00 2001 From: dev1lfreak Date: Fri, 29 May 2026 14:34:02 +0300 Subject: [PATCH 4/5] feat: add 500 csrf exception --- src/application/domain/exceptions/__init__.py | 1 + src/application/domain/exceptions/csrf_error_exception.py | 8 ++++++++ src/presentation/decorators/csrf.py | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/application/domain/exceptions/csrf_error_exception.py diff --git a/src/application/domain/exceptions/__init__.py b/src/application/domain/exceptions/__init__.py index e882cff..d5bb22b 100644 --- a/src/application/domain/exceptions/__init__.py +++ b/src/application/domain/exceptions/__init__.py @@ -7,3 +7,4 @@ from src.application.domain.exceptions.conflict_exception import ConflictExcepti from src.application.domain.exceptions.internal_exception import InternalException from src.application.domain.exceptions.service_unavailable_exception import ServiceUnavailableException from src.application.domain.exceptions.too_many_requests_exception import TooManyRequestsException +from src.application.domain.exceptions.csrf_error_exception import CsrfErrorException diff --git a/src/application/domain/exceptions/csrf_error_exception.py b/src/application/domain/exceptions/csrf_error_exception.py new file mode 100644 index 0000000..cc065b6 --- /dev/null +++ b/src/application/domain/exceptions/csrf_error_exception.py @@ -0,0 +1,8 @@ +from src.application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class CsrfErrorException(ApplicationException): + def __init__(self, message: str = 'CSRF context is invalid', headers: Mapping[str, str] | None = None): + super().__init__(500, message, headers) diff --git a/src/presentation/decorators/csrf.py b/src/presentation/decorators/csrf.py index bc08e31..540bc79 100644 --- a/src/presentation/decorators/csrf.py +++ b/src/presentation/decorators/csrf.py @@ -3,7 +3,7 @@ import inspect from functools import wraps from typing import Callable, Awaitable, Any, Optional, Annotated from fastapi import Request, Header -from src.application.domain.exceptions import InternalException +from src.application.domain.exceptions import CsrfErrorException from src.infrastructure.security import CsrfService @@ -39,7 +39,7 @@ def csrf_protect( break if request is None: - raise InternalException(message='Request is required for CSRF protection') + raise CsrfErrorException(message='Request is required for CSRF protection') csrf = CsrfService() -- 2.49.1 From 35b968d28862ed1ff1c54950dfb13b0951a02ef2 Mon Sep 17 00:00:00 2001 From: dev1lfreak Date: Mon, 1 Jun 2026 15:07:44 +0300 Subject: [PATCH 5/5] feat: add new custom 500 exceptions --- src/application/domain/exceptions/__init__.py | 3 +++ .../domain/exceptions/data_base_error_exception.py | 8 ++++++++ .../domain/exceptions/jwt_error_exception.py | 8 ++++++++ .../domain/exceptions/rate_limit_error_exception.py | 8 ++++++++ .../database/repositories/user_repository.py | 12 ++++++------ src/infrastructure/security/jwt.py | 4 ++-- src/infrastructure/vault/keys.py | 4 ++-- src/presentation/decorators/rate_limit.py | 4 ++-- 8 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 src/application/domain/exceptions/data_base_error_exception.py create mode 100644 src/application/domain/exceptions/jwt_error_exception.py create mode 100644 src/application/domain/exceptions/rate_limit_error_exception.py diff --git a/src/application/domain/exceptions/__init__.py b/src/application/domain/exceptions/__init__.py index d5bb22b..ed764c1 100644 --- a/src/application/domain/exceptions/__init__.py +++ b/src/application/domain/exceptions/__init__.py @@ -8,3 +8,6 @@ from src.application.domain.exceptions.internal_exception import InternalExcepti from src.application.domain.exceptions.service_unavailable_exception import ServiceUnavailableException from src.application.domain.exceptions.too_many_requests_exception import TooManyRequestsException from src.application.domain.exceptions.csrf_error_exception import CsrfErrorException +from src.application.domain.exceptions.jwt_error_exception import JwtErrorException +from src.application.domain.exceptions.data_base_error_exception import DataBaseErrorException +from src.application.domain.exceptions.rate_limit_error_exception import RateLimitErrorException \ No newline at end of file diff --git a/src/application/domain/exceptions/data_base_error_exception.py b/src/application/domain/exceptions/data_base_error_exception.py new file mode 100644 index 0000000..93f790b --- /dev/null +++ b/src/application/domain/exceptions/data_base_error_exception.py @@ -0,0 +1,8 @@ +from src.application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class DataBaseErrorException(ApplicationException): + def __init__(self, message: str = 'Database error occurred', headers: Mapping[str, str] | None = None): + super().__init__(500, message, headers) \ No newline at end of file diff --git a/src/application/domain/exceptions/jwt_error_exception.py b/src/application/domain/exceptions/jwt_error_exception.py new file mode 100644 index 0000000..463c6d8 --- /dev/null +++ b/src/application/domain/exceptions/jwt_error_exception.py @@ -0,0 +1,8 @@ +from src.application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class JwtErrorException(ApplicationException): + def __init__(self, message: str = 'JWT error occurred', headers: Mapping[str, str] | None = None): + super().__init__(500, message, headers) \ No newline at end of file diff --git a/src/application/domain/exceptions/rate_limit_error_exception.py b/src/application/domain/exceptions/rate_limit_error_exception.py new file mode 100644 index 0000000..9e2ab24 --- /dev/null +++ b/src/application/domain/exceptions/rate_limit_error_exception.py @@ -0,0 +1,8 @@ +from src.application.domain.exceptions.application_exceptions import ApplicationException + +from typing import Mapping + + +class RateLimitErrorException(ApplicationException): + def __init__(self, message: str = 'Rate limit error occurred', headers: Mapping[str, str] | None = None): + super().__init__(500, message, headers) \ No newline at end of file diff --git a/src/infrastructure/database/repositories/user_repository.py b/src/infrastructure/database/repositories/user_repository.py index b28543b..462b12d 100644 --- a/src/infrastructure/database/repositories/user_repository.py +++ b/src/infrastructure/database/repositories/user_repository.py @@ -3,7 +3,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import SQLAlchemyError from src.application.contracts import ILogger -from src.application.domain.exceptions import ApplicationException, BadRequestException, InternalException, NotFoundException +from src.application.domain.exceptions import ApplicationException, BadRequestException, DataBaseErrorException, NotFoundException from src.application.abstractions.repositories import IUserRepository from src.application.domain.entities import UserEntity from src.infrastructure.database.models import UserModel @@ -60,7 +60,7 @@ class UserRepository(IUserRepository): raise except SQLAlchemyError as exception: self._logger.exception(str(exception)) - raise InternalException(message=f'Database error: {str(exception)}') + raise DataBaseErrorException(message=f'Database error: {str(exception)}') async def _update_field(self, user_id: str, **fields: object) -> UserEntity: try: @@ -74,7 +74,7 @@ class UserRepository(IUserRepository): raise except SQLAlchemyError as exception: self._logger.exception(str(exception)) - raise InternalException(message=f'Database error: {str(exception)}') + raise DataBaseErrorException(message=f'Database error: {str(exception)}') async def set_phone(self, user_id: str, phone: str) -> UserEntity: return await self._update_field(user_id, phone=phone) @@ -100,7 +100,7 @@ class UserRepository(IUserRepository): raise except SQLAlchemyError as exception: self._logger.exception(str(exception)) - raise InternalException(message=f'Database error: {str(exception)}') + raise DataBaseErrorException(message=f'Database error: {str(exception)}') async def set_password(self, user_id: str, password_hash: str) -> UserEntity: return await self._update_field(user_id, password_hash=password_hash) @@ -121,7 +121,7 @@ class UserRepository(IUserRepository): return result.scalar_one_or_none() is not None except SQLAlchemyError as exception: self._logger.exception(str(exception)) - raise InternalException(message=f'Database error: {str(exception)}') + raise DataBaseErrorException(message=f'Database error: {str(exception)}') async def get_user_by_email(self, email: str) -> UserEntity | None: try: @@ -139,4 +139,4 @@ class UserRepository(IUserRepository): return self._to_entity(user) except SQLAlchemyError as exception: self._logger.exception(str(exception)) - raise InternalException(message=f'Database error: {str(exception)}') + raise DataBaseErrorException(message=f'Database error: {str(exception)}') diff --git a/src/infrastructure/security/jwt.py b/src/infrastructure/security/jwt.py index 7102708..4a1be90 100644 --- a/src/infrastructure/security/jwt.py +++ b/src/infrastructure/security/jwt.py @@ -2,7 +2,7 @@ from __future__ import annotations from jose import jwt, ExpiredSignatureError, JWTError from src.application.contracts import ILogger, IJwtService from src.application.domain.dto import AccessTokenPayload -from src.application.domain.exceptions import ApplicationException, UnauthorizedException, InternalException +from src.application.domain.exceptions import ApplicationException, UnauthorizedException, JwtErrorException from src.infrastructure.config.settings import settings from src.infrastructure.vault import JwtKeyStore @@ -106,4 +106,4 @@ class JwtService(IJwtService): except Exception as exception: self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}') - raise InternalException(message='JWT decode failed') \ No newline at end of file + raise JwtErrorException(message='JWT decode failed') \ No newline at end of file diff --git a/src/infrastructure/vault/keys.py b/src/infrastructure/vault/keys.py index 9da61da..17cd72b 100644 --- a/src/infrastructure/vault/keys.py +++ b/src/infrastructure/vault/keys.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timezone from src.application.domain.dto import JwtPublicKeySet, JwtPublicKey -from src.application.domain.exceptions import InternalException +from src.application.domain.exceptions import JwtErrorException from src.infrastructure.vault.client import VaultClient @@ -52,7 +52,7 @@ class JwtKeyStore: @classmethod def get_instance(cls) -> 'JwtKeyStore': if cls._instance is None: - raise InternalException(message='JwtKeyStore not initialized') + raise JwtErrorException(message='JwtKeyStore not initialized') return cls._instance def _read_keyset_sync(self) -> JwtPublicKeySet: diff --git a/src/presentation/decorators/rate_limit.py b/src/presentation/decorators/rate_limit.py index 964552b..2de8580 100644 --- a/src/presentation/decorators/rate_limit.py +++ b/src/presentation/decorators/rate_limit.py @@ -7,7 +7,7 @@ from fastapi import Request from redis.asyncio.client import Redis from src.application.contracts import ILogger from src.application.domain.exceptions import ( - InternalException, + RateLimitErrorException, ServiceUnavailableException, TooManyRequestsException, ) @@ -128,7 +128,7 @@ def rate_limit( ident = _call_key_builder(key_builder, request, args, kwargs) # type: ignore[arg-type] except Exception as e: logger.error(f'RateLimit key_builder failed error={str(e)}') - raise InternalException(message='Rate limiter key_builder failed') + raise RateLimitErrorException(message='Rate limiter key_builder failed') route = request.url.path method = request.method -- 2.49.1