Таска 3: разделение кастомных эксепшенов #2

Open
skvachuk wants to merge 5 commits from feature/3 into develop
31 changed files with 219 additions and 176 deletions

View File

@@ -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)

View File

@@ -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.')

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -1,11 +1,13 @@
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
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

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -0,0 +1,8 @@
from src.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)

View File

@@ -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)}')

View File

@@ -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)

View File

@@ -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, JwtErrorException
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')
raise JwtErrorException(message='JWT decode failed')

View File

@@ -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 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 ApplicationException(status_code=500, message='JwtKeyStore not initialized')
raise JwtErrorException(message='JwtKeyStore not initialized')
return cls._instance
def _read_keyset_sync(self) -> JwtPublicKeySet:

View File

@@ -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 CsrfErrorException
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 CsrfErrorException(message='Request is required for CSRF protection')

Лучше что-то более конкретное. По валидации не был получен объект Request, значит что-то связанное с некорректным формированием запроса, либо CsrfErrorException - если более общий вариант

Лучше что-то более конкретное. По валидации не был получен объект Request, значит что-то связанное с некорректным формированием запроса, либо CsrfErrorException - если более общий вариант
csrf = CsrfService()

View File

@@ -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 (
RateLimitErrorException,
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 RateLimitErrorException(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)},
)