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)}, )