Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21cf44cebc | |||
| b73d5231f5 |
@@ -1,6 +1,6 @@
|
|||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ILogger, ICache
|
from src.application.contracts import IHashService, ILogger, ICache
|
||||||
from src.application.domain.exceptions import BadRequestException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
|
|
||||||
@@ -30,16 +30,16 @@ class ChangeEmailCompleteCommand:
|
|||||||
cached_user_id = await self._cache.get(new_code_key)
|
cached_user_id = await self._cache.get(new_code_key)
|
||||||
if not cached_user_id:
|
if not cached_user_id:
|
||||||
self._logger.info(f'Change email complete failed: code not found (user_id={user_id})')
|
self._logger.info(f'Change email complete failed: code not found (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
if cached_user_id != user_id:
|
if cached_user_id != user_id:
|
||||||
self._logger.info(f'Change email complete failed: code-user mismatch (user_id={user_id})')
|
self._logger.info(f'Change email complete failed: code-user mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
raw_value = await self._cache.get(new_user_key)
|
raw_value = await self._cache.get(new_user_key)
|
||||||
if not raw_value:
|
if not raw_value:
|
||||||
self._logger.info(f'Change email complete failed: user key missing (user_id={user_id})')
|
self._logger.info(f'Change email complete failed: user key missing (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
separator_idx = raw_value.index(':')
|
separator_idx = raw_value.index(':')
|
||||||
code_hash = raw_value[:separator_idx]
|
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)
|
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
||||||
if not ok:
|
if not ok:
|
||||||
self._logger.info(f'Change email complete failed: code hash mismatch (user_id={user_id})')
|
self._logger.info(f'Change email complete failed: code hash mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
user = await self._unit_of_work.user_repository.set_email(user_id=user_id, email=new_email)
|
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)
|
await self._cache.set_user(user_id, user)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
||||||
from src.application.domain.exceptions import BadRequestException, ConflictException, ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
@@ -41,32 +41,32 @@ class ChangeEmailConfirmOldCommand:
|
|||||||
cached_user_id = await self._cache.get(old_code_key)
|
cached_user_id = await self._cache.get(old_code_key)
|
||||||
if not cached_user_id:
|
if not cached_user_id:
|
||||||
self._logger.info(f'Change email confirm-old failed: code not found (user_id={user_id})')
|
self._logger.info(f'Change email confirm-old failed: code not found (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
if cached_user_id != user_id:
|
if cached_user_id != user_id:
|
||||||
self._logger.info(f'Change email confirm-old failed: code-user mismatch (user_id={user_id})')
|
self._logger.info(f'Change email confirm-old failed: code-user mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
code_hash = await self._cache.get(old_user_key)
|
code_hash = await self._cache.get(old_user_key)
|
||||||
if not code_hash:
|
if not code_hash:
|
||||||
self._logger.info(f'Change email confirm-old failed: user key missing (user_id={user_id})')
|
self._logger.info(f'Change email confirm-old failed: user key missing (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
||||||
if not ok:
|
if not ok:
|
||||||
self._logger.info(f'Change email confirm-old failed: code hash mismatch (user_id={user_id})')
|
self._logger.info(f'Change email confirm-old failed: code hash mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id)
|
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():
|
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})')
|
self._logger.info(f'Change email confirm-old failed: new email same as current (user_id={user_id})')
|
||||||
raise BadRequestException(message='New email must differ from the current one')
|
raise ApplicationException(400, 'New email must differ from the current one')
|
||||||
|
|
||||||
email_taken = await self._unit_of_work.user_repository.email_exists(email=new_email)
|
email_taken = await self._unit_of_work.user_repository.email_exists(email=new_email)
|
||||||
if email_taken:
|
if email_taken:
|
||||||
self._logger.info(f'Change email confirm-old failed: new email already taken (user_id={user_id})')
|
self._logger.info(f'Change email confirm-old failed: new email already taken (user_id={user_id})')
|
||||||
raise ConflictException(message='Email already in use')
|
raise ApplicationException(409, 'Email already in use')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._cache.delete(old_code_key)
|
await self._cache.delete(old_code_key)
|
||||||
@@ -94,7 +94,7 @@ class ChangeEmailConfirmOldCommand:
|
|||||||
if not saved:
|
if not saved:
|
||||||
await self._cache.delete(new_code_key)
|
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}')
|
self._logger.error(f'Change email confirm-old failed: cannot save new code hash for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
message_id = str(ULID())
|
message_id = str(ULID())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
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'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)}')
|
self._logger.error(f'Failed to publish change email new code for user_id={user_id}: {str(exception)}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.error(f'Change email confirm-old failed: code space exhausted for user_id={user_id}')
|
self._logger.error(f'Change email confirm-old failed: code space exhausted for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
||||||
from src.application.domain.exceptions import NotFoundException, TooManyRequestsException, ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
@@ -38,7 +38,7 @@ class ChangeEmailStartCommand:
|
|||||||
|
|
||||||
if not user.email:
|
if not user.email:
|
||||||
self._logger.warning(f'User {user_id} does not have an email address')
|
self._logger.warning(f'User {user_id} does not have an email address')
|
||||||
raise NotFoundException(message=f'User {user_id} does not have an email address')
|
raise ApplicationException(404, f'User {user_id} does not have an email address')
|
||||||
|
|
||||||
trace_id = trace_id_var.get()
|
trace_id = trace_id_var.get()
|
||||||
if not trace_id or trace_id == 'N/A':
|
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)
|
locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL)
|
||||||
if not locked:
|
if not locked:
|
||||||
self._logger.info(f'Change email throttled by lock (user_id={user_id})')
|
self._logger.info(f'Change email throttled by lock (user_id={user_id})')
|
||||||
raise TooManyRequestsException(message='Too many requests. Please wait.')
|
raise ApplicationException(429, 'Too many requests. Please wait.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_key = f'{USER_PREFIX}{user_id}'
|
user_key = f'{USER_PREFIX}{user_id}'
|
||||||
@@ -56,7 +56,7 @@ class ChangeEmailStartCommand:
|
|||||||
existing = await self._cache.get(user_key)
|
existing = await self._cache.get(user_key)
|
||||||
if existing:
|
if existing:
|
||||||
self._logger.info(f'Change email denied: code already exists for user_id={user_id}')
|
self._logger.info(f'Change email denied: code already exists for user_id={user_id}')
|
||||||
raise TooManyRequestsException(message='Code already sent. Please wait before retrying.')
|
raise ApplicationException(429, 'Code already sent. Please wait before retrying.')
|
||||||
|
|
||||||
for _ in range(MAX_ATTEMPTS):
|
for _ in range(MAX_ATTEMPTS):
|
||||||
code = f'{secrets.randbelow(1_000_000):06d}'
|
code = f'{secrets.randbelow(1_000_000):06d}'
|
||||||
@@ -72,7 +72,7 @@ class ChangeEmailStartCommand:
|
|||||||
if not saved:
|
if not saved:
|
||||||
await self._cache.delete(code_key)
|
await self._cache.delete(code_key)
|
||||||
self._logger.error(f'Change email failed: cannot save code hash for user_id={user_id}')
|
self._logger.error(f'Change email failed: cannot save code hash for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
message_id = str(ULID())
|
message_id = str(ULID())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
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'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)}')
|
self._logger.error(f'Failed to publish change email old code for user_id={user_id}: {str(exception)}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.error(f'Change email failed: code space exhausted for user_id={user_id}')
|
self._logger.error(f'Change email failed: code space exhausted for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await self._cache.delete(lock_key)
|
await self._cache.delete(lock_key)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ILogger, ICache
|
from src.application.contracts import IHashService, ILogger, ICache
|
||||||
from src.application.domain.exceptions import BadRequestException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
|
|
||||||
@@ -36,33 +36,33 @@ class ChangePasswordCompleteCommand:
|
|||||||
|
|
||||||
if new_password != confirm_password:
|
if new_password != confirm_password:
|
||||||
self._logger.info(f'Change password failed: passwords do not match (user_id={user_id})')
|
self._logger.info(f'Change password failed: passwords do not match (user_id={user_id})')
|
||||||
raise BadRequestException(message='Passwords do not match')
|
raise ApplicationException(400, 'Passwords do not match')
|
||||||
|
|
||||||
cached_user_id = await self._cache.get(code_key)
|
cached_user_id = await self._cache.get(code_key)
|
||||||
if not cached_user_id:
|
if not cached_user_id:
|
||||||
self._logger.info(f'Change password failed: code not found (user_id={user_id})')
|
self._logger.info(f'Change password failed: code not found (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
if cached_user_id != user_id:
|
if cached_user_id != user_id:
|
||||||
self._logger.info(f'Change password failed: code-user mismatch (user_id={user_id})')
|
self._logger.info(f'Change password failed: code-user mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
code_hash = await self._cache.get(user_key)
|
code_hash = await self._cache.get(user_key)
|
||||||
if not code_hash:
|
if not code_hash:
|
||||||
self._logger.info(f'Change password failed: user key missing (user_id={user_id})')
|
self._logger.info(f'Change password failed: user key missing (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
||||||
if not ok:
|
if not ok:
|
||||||
self._logger.info(f'Change password failed: code hash mismatch (user_id={user_id})')
|
self._logger.info(f'Change password failed: code hash mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
current_password_hash = await self._unit_of_work.user_repository.get_password_hash(user_id=user_id)
|
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)
|
is_same = await self._hash_service.verify(hashed_value=current_password_hash, plain_value=new_password)
|
||||||
if is_same:
|
if is_same:
|
||||||
self._logger.info(f'Change password failed: new password same as current (user_id={user_id})')
|
self._logger.info(f'Change password failed: new password same as current (user_id={user_id})')
|
||||||
raise BadRequestException(message='New password must differ from the current one')
|
raise ApplicationException(400, 'New password must differ from the current one')
|
||||||
|
|
||||||
new_password_hash = await self._hash_service.hash(new_password)
|
new_password_hash = await self._hash_service.hash(new_password)
|
||||||
user = await self._unit_of_work.user_repository.set_password(
|
user = await self._unit_of_work.user_repository.set_password(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
||||||
from src.application.domain.exceptions import NotFoundException, TooManyRequestsException, ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
@@ -38,7 +38,7 @@ class ChangePasswordStartCommand:
|
|||||||
|
|
||||||
if not user.email:
|
if not user.email:
|
||||||
self._logger.warning(f'User {user_id} does not have an email address')
|
self._logger.warning(f'User {user_id} does not have an email address')
|
||||||
raise NotFoundException(message=f'User {user_id} does not have an email address')
|
raise ApplicationException(404, f'User {user_id} does not have an email address')
|
||||||
|
|
||||||
trace_id = trace_id_var.get()
|
trace_id = trace_id_var.get()
|
||||||
if not trace_id or trace_id == 'N/A':
|
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)
|
locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL)
|
||||||
if not locked:
|
if not locked:
|
||||||
self._logger.info(f'Change password throttled by lock (user_id={user_id})')
|
self._logger.info(f'Change password throttled by lock (user_id={user_id})')
|
||||||
raise TooManyRequestsException(message='Too many requests. Please wait.')
|
raise ApplicationException(429, 'Too many requests. Please wait.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_key = f'{USER_PREFIX}{user_id}'
|
user_key = f'{USER_PREFIX}{user_id}'
|
||||||
@@ -56,7 +56,7 @@ class ChangePasswordStartCommand:
|
|||||||
existing = await self._cache.get(user_key)
|
existing = await self._cache.get(user_key)
|
||||||
if existing:
|
if existing:
|
||||||
self._logger.info(f'Change password denied: code already exists for user_id={user_id}')
|
self._logger.info(f'Change password denied: code already exists for user_id={user_id}')
|
||||||
raise TooManyRequestsException(message='Code already sent. Please wait before retrying.')
|
raise ApplicationException(429, 'Code already sent. Please wait before retrying.')
|
||||||
|
|
||||||
for _ in range(MAX_ATTEMPTS):
|
for _ in range(MAX_ATTEMPTS):
|
||||||
code = f'{secrets.randbelow(1_000_000):06d}'
|
code = f'{secrets.randbelow(1_000_000):06d}'
|
||||||
@@ -72,7 +72,7 @@ class ChangePasswordStartCommand:
|
|||||||
if not saved:
|
if not saved:
|
||||||
await self._cache.delete(code_key)
|
await self._cache.delete(code_key)
|
||||||
self._logger.error(f'Change password failed: cannot save code hash for user_id={user_id}')
|
self._logger.error(f'Change password failed: cannot save code hash for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
message_id = str(ULID())
|
message_id = str(ULID())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
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'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)}')
|
self._logger.error(f'Failed to publish change password email for user_id={user_id}: {str(exception)}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.error(f'Change password failed: code space exhausted for user_id={user_id}')
|
self._logger.error(f'Change password failed: code space exhausted for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await self._cache.delete(lock_key)
|
await self._cache.delete(lock_key)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ILogger, ICache
|
from src.application.contracts import IHashService, ILogger, ICache
|
||||||
from src.application.domain.exceptions import BadRequestException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
|
|
||||||
@@ -38,33 +38,33 @@ class ForgotPasswordCompleteCommand:
|
|||||||
|
|
||||||
if new_password != confirm_password:
|
if new_password != confirm_password:
|
||||||
self._logger.info('Forgot password failed: passwords do not match')
|
self._logger.info('Forgot password failed: passwords do not match')
|
||||||
raise BadRequestException(message='Passwords do not match')
|
raise ApplicationException(400, 'Passwords do not match')
|
||||||
|
|
||||||
code_key = f'{CODE_PREFIX}{code}'
|
code_key = f'{CODE_PREFIX}{code}'
|
||||||
cached_email = await self._cache.get(code_key)
|
cached_email = await self._cache.get(code_key)
|
||||||
if not cached_email:
|
if not cached_email:
|
||||||
self._logger.info('Forgot password failed: code not found')
|
self._logger.info('Forgot password failed: code not found')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
if cached_email != normalized:
|
if cached_email != normalized:
|
||||||
self._logger.info('Forgot password failed: code-email mismatch')
|
self._logger.info('Forgot password failed: code-email mismatch')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
email_key = f'{EMAIL_PREFIX}{normalized}'
|
email_key = f'{EMAIL_PREFIX}{normalized}'
|
||||||
code_hash = await self._cache.get(email_key)
|
code_hash = await self._cache.get(email_key)
|
||||||
if not code_hash:
|
if not code_hash:
|
||||||
self._logger.info('Forgot password failed: email key missing')
|
self._logger.info('Forgot password failed: email key missing')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
||||||
if not ok:
|
if not ok:
|
||||||
self._logger.info('Forgot password failed: code hash mismatch')
|
self._logger.info('Forgot password failed: code hash mismatch')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
user = await self._unit_of_work.user_repository.get_user_by_email(normalized)
|
user = await self._unit_of_work.user_repository.get_user_by_email(normalized)
|
||||||
if user is None:
|
if user is None:
|
||||||
self._logger.info('Forgot password failed: user not found after valid code')
|
self._logger.info('Forgot password failed: user not found after valid code')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
new_password_hash = await self._hash_service.hash(new_password)
|
new_password_hash = await self._hash_service.hash(new_password)
|
||||||
user = await self._unit_of_work.user_repository.set_password(
|
user = await self._unit_of_work.user_repository.set_password(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
||||||
from src.application.domain.exceptions import TooManyRequestsException, ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.infrastructure.database.decorators import transactional
|
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)
|
locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL)
|
||||||
if not locked:
|
if not locked:
|
||||||
self._logger.info(f'Forgot password throttled by lock (user_id={user.id})')
|
self._logger.info(f'Forgot password throttled by lock (user_id={user.id})')
|
||||||
raise TooManyRequestsException(message='Too many requests. Please wait.')
|
raise ApplicationException(429, 'Too many requests. Please wait.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
email_key = f'{EMAIL_PREFIX}{normalized}'
|
email_key = f'{EMAIL_PREFIX}{normalized}'
|
||||||
@@ -60,7 +60,7 @@ class ForgotPasswordStartCommand:
|
|||||||
existing = await self._cache.get(email_key)
|
existing = await self._cache.get(email_key)
|
||||||
if existing:
|
if existing:
|
||||||
self._logger.info(f'Forgot password denied: code already exists for user_id={user.id}')
|
self._logger.info(f'Forgot password denied: code already exists for user_id={user.id}')
|
||||||
raise TooManyRequestsException(message='Code already sent. Please wait before retrying.')
|
raise ApplicationException(429, 'Code already sent. Please wait before retrying.')
|
||||||
|
|
||||||
for _ in range(MAX_ATTEMPTS):
|
for _ in range(MAX_ATTEMPTS):
|
||||||
code = f'{secrets.randbelow(1_000_000):06d}'
|
code = f'{secrets.randbelow(1_000_000):06d}'
|
||||||
@@ -76,7 +76,7 @@ class ForgotPasswordStartCommand:
|
|||||||
if not saved:
|
if not saved:
|
||||||
await self._cache.delete(code_key)
|
await self._cache.delete(code_key)
|
||||||
self._logger.error(f'Forgot password failed: cannot save code hash for user_id={user.id}')
|
self._logger.error(f'Forgot password failed: cannot save code hash for user_id={user.id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
message_id = str(ULID())
|
message_id = str(ULID())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
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)}')
|
self._logger.error(f'Failed to publish forgot password email for user_id={user.id}: {str(exception)}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.error(f'Forgot password failed: code space exhausted for user_id={user.id}')
|
self._logger.error(f'Forgot password failed: code space exhausted for user_id={user.id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await self._cache.delete(lock_key)
|
await self._cache.delete(lock_key)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ILogger, ICache
|
from src.application.contracts import IHashService, ILogger, ICache
|
||||||
from src.application.domain.entities import UserEntity
|
from src.application.domain.entities import UserEntity
|
||||||
from src.application.domain.exceptions import BadRequestException, ConflictException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.database.decorators import transactional
|
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)
|
user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id)
|
||||||
if user.encrypted_mnemonic is not None:
|
if user.encrypted_mnemonic is not None:
|
||||||
self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}')
|
self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}')
|
||||||
raise ConflictException(message='Encrypted mnemonic already set and cannot be changed')
|
raise ApplicationException(409, 'Encrypted mnemonic already set and cannot be changed')
|
||||||
|
|
||||||
cached_user_id = await self._cache.get(code_key)
|
cached_user_id = await self._cache.get(code_key)
|
||||||
if not cached_user_id:
|
if not cached_user_id:
|
||||||
self._logger.info(f'Encrypted mnemonic set failed: code not found (user_id={user_id})')
|
self._logger.info(f'Encrypted mnemonic set failed: code not found (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
if cached_user_id != user_id:
|
if cached_user_id != user_id:
|
||||||
self._logger.info(f'Encrypted mnemonic set failed: code-user mismatch (user_id={user_id})')
|
self._logger.info(f'Encrypted mnemonic set failed: code-user mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
code_hash = await self._cache.get(user_key)
|
code_hash = await self._cache.get(user_key)
|
||||||
if not code_hash:
|
if not code_hash:
|
||||||
self._logger.info(f'Encrypted mnemonic set failed: user key missing (user_id={user_id})')
|
self._logger.info(f'Encrypted mnemonic set failed: user key missing (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
||||||
if not ok:
|
if not ok:
|
||||||
self._logger.info(f'Encrypted mnemonic set failed: code hash mismatch (user_id={user_id})')
|
self._logger.info(f'Encrypted mnemonic set failed: code hash mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
user = await self._unit_of_work.user_repository.set_encrypted_mnemonic(
|
user = await self._unit_of_work.user_repository.set_encrypted_mnemonic(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
||||||
from src.application.domain.exceptions import ConflictException, NotFoundException, TooManyRequestsException, ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
@@ -38,11 +38,11 @@ class SetEncryptedMnemonicStartCommand:
|
|||||||
|
|
||||||
if user.encrypted_mnemonic is not None:
|
if user.encrypted_mnemonic is not None:
|
||||||
self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}')
|
self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}')
|
||||||
raise ConflictException(message='Encrypted mnemonic already set and cannot be changed')
|
raise ApplicationException(409, 'Encrypted mnemonic already set and cannot be changed')
|
||||||
|
|
||||||
if not user.email:
|
if not user.email:
|
||||||
self._logger.warning(f'User {user_id} does not have an email address')
|
self._logger.warning(f'User {user_id} does not have an email address')
|
||||||
raise NotFoundException(message=f'User {user_id} does not have an email address')
|
raise ApplicationException(404, f'User {user_id} does not have an email address')
|
||||||
|
|
||||||
trace_id = trace_id_var.get()
|
trace_id = trace_id_var.get()
|
||||||
if not trace_id or trace_id == 'N/A':
|
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)
|
locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL)
|
||||||
if not locked:
|
if not locked:
|
||||||
self._logger.info(f'Encrypted mnemonic set throttled by lock (user_id={user_id})')
|
self._logger.info(f'Encrypted mnemonic set throttled by lock (user_id={user_id})')
|
||||||
raise TooManyRequestsException(message='Too many requests. Please wait.')
|
raise ApplicationException(429, 'Too many requests. Please wait.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_key = f'{USER_PREFIX}{user_id}'
|
user_key = f'{USER_PREFIX}{user_id}'
|
||||||
@@ -60,7 +60,7 @@ class SetEncryptedMnemonicStartCommand:
|
|||||||
existing = await self._cache.get(user_key)
|
existing = await self._cache.get(user_key)
|
||||||
if existing:
|
if existing:
|
||||||
self._logger.info(f'Encrypted mnemonic set denied: code already exists for user_id={user_id}')
|
self._logger.info(f'Encrypted mnemonic set denied: code already exists for user_id={user_id}')
|
||||||
raise TooManyRequestsException(message='Code already sent. Please wait before retrying.')
|
raise ApplicationException(429, 'Code already sent. Please wait before retrying.')
|
||||||
|
|
||||||
for _ in range(MAX_ATTEMPTS):
|
for _ in range(MAX_ATTEMPTS):
|
||||||
code = f'{secrets.randbelow(1_000_000):06d}'
|
code = f'{secrets.randbelow(1_000_000):06d}'
|
||||||
@@ -76,7 +76,7 @@ class SetEncryptedMnemonicStartCommand:
|
|||||||
if not saved:
|
if not saved:
|
||||||
await self._cache.delete(code_key)
|
await self._cache.delete(code_key)
|
||||||
self._logger.error(f'Encrypted mnemonic set failed: cannot save code hash for user_id={user_id}')
|
self._logger.error(f'Encrypted mnemonic set failed: cannot save code hash for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
message_id = str(ULID())
|
message_id = str(ULID())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
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'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)}')
|
self._logger.error(f'Failed to publish encrypted mnemonic set email for user_id={user_id}: {str(exception)}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.error(f'Encrypted mnemonic set failed: code space exhausted for user_id={user_id}')
|
self._logger.error(f'Encrypted mnemonic set failed: code space exhausted for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await self._cache.delete(lock_key)
|
await self._cache.delete(lock_key)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ILogger, ICache
|
from src.application.contracts import IHashService, ILogger, ICache
|
||||||
from src.application.domain.entities import UserEntity
|
from src.application.domain.entities import UserEntity
|
||||||
from src.application.domain.exceptions import BadRequestException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
|
|
||||||
@@ -39,21 +39,21 @@ class UpdateBankDetailsCompleteCommand:
|
|||||||
cached_user_id = await self._cache.get(code_key)
|
cached_user_id = await self._cache.get(code_key)
|
||||||
if not cached_user_id:
|
if not cached_user_id:
|
||||||
self._logger.info(f'Bank details update failed: code not found (user_id={user_id})')
|
self._logger.info(f'Bank details update failed: code not found (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
if cached_user_id != user_id:
|
if cached_user_id != user_id:
|
||||||
self._logger.info(f'Bank details update failed: code-user mismatch (user_id={user_id})')
|
self._logger.info(f'Bank details update failed: code-user mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
code_hash = await self._cache.get(user_key)
|
code_hash = await self._cache.get(user_key)
|
||||||
if not code_hash:
|
if not code_hash:
|
||||||
self._logger.info(f'Bank details update failed: user key missing (user_id={user_id})')
|
self._logger.info(f'Bank details update failed: user key missing (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
|
||||||
if not ok:
|
if not ok:
|
||||||
self._logger.info(f'Bank details update failed: code hash mismatch (user_id={user_id})')
|
self._logger.info(f'Bank details update failed: code hash mismatch (user_id={user_id})')
|
||||||
raise BadRequestException(message='Invalid or expired code')
|
raise ApplicationException(400, 'Invalid or expired code')
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
if passport_data is not None:
|
if passport_data is not None:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
from src.application.contracts import IHashService, ICache, ILogger, IQueueMessanger
|
||||||
from src.application.domain.exceptions import NotFoundException, TooManyRequestsException, ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
@@ -38,7 +38,7 @@ class UpdateBankDetailsStartCommand:
|
|||||||
|
|
||||||
if not user.email:
|
if not user.email:
|
||||||
self._logger.warning(f'User {user_id} does not have an email address')
|
self._logger.warning(f'User {user_id} does not have an email address')
|
||||||
raise NotFoundException(message=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')
|
||||||
|
|
||||||
trace_id = trace_id_var.get()
|
trace_id = trace_id_var.get()
|
||||||
if not trace_id or trace_id == 'N/A':
|
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)
|
locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL)
|
||||||
if not locked:
|
if not locked:
|
||||||
self._logger.info(f'Bank details update throttled by lock (user_id={user_id})')
|
self._logger.info(f'Bank details update throttled by lock (user_id={user_id})')
|
||||||
raise TooManyRequestsException(message='Too many requests. Please wait.')
|
raise ApplicationException(429, 'Too many requests. Please wait.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_key = f'{USER_PREFIX}{user_id}'
|
user_key = f'{USER_PREFIX}{user_id}'
|
||||||
@@ -56,7 +56,7 @@ class UpdateBankDetailsStartCommand:
|
|||||||
existing = await self._cache.get(user_key)
|
existing = await self._cache.get(user_key)
|
||||||
if existing:
|
if existing:
|
||||||
self._logger.info(f'Bank details update denied: code already exists for user_id={user_id}')
|
self._logger.info(f'Bank details update denied: code already exists for user_id={user_id}')
|
||||||
raise TooManyRequestsException(message='Code already sent. Please wait before retrying.')
|
raise ApplicationException(429, 'Code already sent. Please wait before retrying.')
|
||||||
|
|
||||||
for _ in range(MAX_ATTEMPTS):
|
for _ in range(MAX_ATTEMPTS):
|
||||||
code = f'{secrets.randbelow(1_000_000):06d}'
|
code = f'{secrets.randbelow(1_000_000):06d}'
|
||||||
@@ -72,7 +72,7 @@ class UpdateBankDetailsStartCommand:
|
|||||||
if not saved:
|
if not saved:
|
||||||
await self._cache.delete(code_key)
|
await self._cache.delete(code_key)
|
||||||
self._logger.error(f'Bank details update failed: cannot save code hash for user_id={user_id}')
|
self._logger.error(f'Bank details update failed: cannot save code hash for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
message_id = str(ULID())
|
message_id = str(ULID())
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
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'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)}')
|
self._logger.error(f'Failed to publish bank details update email for user_id={user_id}: {str(exception)}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._logger.error(f'Bank details update failed: code space exhausted for user_id={user_id}')
|
self._logger.error(f'Bank details update failed: code space exhausted for user_id={user_id}')
|
||||||
raise ServiceUnavailableException(message='Temporary error. Please try again.')
|
raise ApplicationException(503, 'Temporary error. Please try again.')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await self._cache.delete(lock_key)
|
await self._cache.delete(lock_key)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
from src.application.domain.exceptions.application_exceptions import ApplicationException
|
from src.application.domain.exceptions.application_exceptions import (
|
||||||
from src.application.domain.exceptions.bad_request_exception import BadRequestException
|
ApplicationException,
|
||||||
from src.application.domain.exceptions.unauthorized_exception import UnauthorizedException
|
BadRequestException,
|
||||||
from src.application.domain.exceptions.forbidden_exception import ForbiddenException
|
ConflictException,
|
||||||
from src.application.domain.exceptions.not_found_exception import NotFoundException
|
ForbiddenException,
|
||||||
from src.application.domain.exceptions.conflict_exception import ConflictException
|
InternalException,
|
||||||
from src.application.domain.exceptions.internal_exception import InternalException
|
NotFoundException,
|
||||||
from src.application.domain.exceptions.service_unavailable_exception import ServiceUnavailableException
|
ServiceUnavailableException,
|
||||||
from src.application.domain.exceptions.too_many_requests_exception import TooManyRequestsException
|
TooManyRequestsException,
|
||||||
from src.application.domain.exceptions.csrf_error_exception import CsrfErrorException
|
UnauthorizedException,
|
||||||
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
|
|
||||||
|
|||||||
@@ -17,3 +17,43 @@ class ApplicationException(Exception):
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f'{self.status_code}: {self.message}'
|
return f'{self.status_code}: {self.message}'
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(400, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Unauthorized', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(401, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Forbidden', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(403, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Not found', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(404, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(409, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyRequestsException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(429, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailableException(ApplicationException):
|
||||||
|
def __init__(self, message: str, headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(503, message, headers)
|
||||||
|
|
||||||
|
|
||||||
|
class InternalException(ApplicationException):
|
||||||
|
def __init__(self, message: str = 'Internal Server Error', headers: Mapping[str, str] | None = None):
|
||||||
|
super().__init__(500, message, headers)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -3,7 +3,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.exceptions import ApplicationException, BadRequestException, DataBaseErrorException, NotFoundException
|
from src.application.domain.exceptions import ApplicationException, BadRequestException, InternalException, NotFoundException
|
||||||
from src.application.abstractions.repositories import IUserRepository
|
from src.application.abstractions.repositories import IUserRepository
|
||||||
from src.application.domain.entities import UserEntity
|
from src.application.domain.entities import UserEntity
|
||||||
from src.infrastructure.database.models import UserModel
|
from src.infrastructure.database.models import UserModel
|
||||||
@@ -60,7 +60,7 @@ class UserRepository(IUserRepository):
|
|||||||
raise
|
raise
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise DataBaseErrorException(message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def _update_field(self, user_id: str, **fields: object) -> UserEntity:
|
async def _update_field(self, user_id: str, **fields: object) -> UserEntity:
|
||||||
try:
|
try:
|
||||||
@@ -74,7 +74,7 @@ class UserRepository(IUserRepository):
|
|||||||
raise
|
raise
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise DataBaseErrorException(message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def set_phone(self, user_id: str, phone: str) -> UserEntity:
|
async def set_phone(self, user_id: str, phone: str) -> UserEntity:
|
||||||
return await self._update_field(user_id, phone=phone)
|
return await self._update_field(user_id, phone=phone)
|
||||||
@@ -100,7 +100,7 @@ class UserRepository(IUserRepository):
|
|||||||
raise
|
raise
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise DataBaseErrorException(message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
||||||
return await self._update_field(user_id, password_hash=password_hash)
|
return await self._update_field(user_id, password_hash=password_hash)
|
||||||
@@ -121,7 +121,7 @@ class UserRepository(IUserRepository):
|
|||||||
return result.scalar_one_or_none() is not None
|
return result.scalar_one_or_none() is not None
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise DataBaseErrorException(message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|
||||||
async def get_user_by_email(self, email: str) -> UserEntity | None:
|
async def get_user_by_email(self, email: str) -> UserEntity | None:
|
||||||
try:
|
try:
|
||||||
@@ -139,4 +139,4 @@ class UserRepository(IUserRepository):
|
|||||||
return self._to_entity(user)
|
return self._to_entity(user)
|
||||||
except SQLAlchemyError as exception:
|
except SQLAlchemyError as exception:
|
||||||
self._logger.exception(str(exception))
|
self._logger.exception(str(exception))
|
||||||
raise DataBaseErrorException(message=f'Database error: {str(exception)}')
|
raise InternalException(message=f'Database error: {str(exception)}')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import secrets
|
|||||||
from typing import Any, Optional, Mapping
|
from typing import Any, Optional, Mapping
|
||||||
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
|
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
|
||||||
from src.application.contracts import ICsrfService
|
from src.application.contracts import ICsrfService
|
||||||
from src.application.domain.exceptions import ForbiddenException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config.settings import settings
|
from src.infrastructure.config.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -42,12 +42,21 @@ class CsrfService(ICsrfService):
|
|||||||
try:
|
try:
|
||||||
data = self._serializer.loads(token, max_age=self.TTL_SECONDS)
|
data = self._serializer.loads(token, max_age=self.TTL_SECONDS)
|
||||||
except SignatureExpired:
|
except SignatureExpired:
|
||||||
raise ForbiddenException(message='CSRF token expired')
|
raise ApplicationException(
|
||||||
|
status_code=403,
|
||||||
|
message='CSRF token expired',
|
||||||
|
)
|
||||||
except BadSignature:
|
except BadSignature:
|
||||||
raise ForbiddenException(message='CSRF token invalid')
|
raise ApplicationException(
|
||||||
|
status_code=403,
|
||||||
|
message='CSRF token invalid',
|
||||||
|
)
|
||||||
|
|
||||||
if expected_subject is not None and data.get('sub') != expected_subject:
|
if expected_subject is not None and data.get('sub') != expected_subject:
|
||||||
raise ForbiddenException(message='CSRF token subject mismatch')
|
raise ApplicationException(
|
||||||
|
status_code=403,
|
||||||
|
message='CSRF token subject mismatch',
|
||||||
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -58,9 +67,15 @@ class CsrfService(ICsrfService):
|
|||||||
|
|
||||||
def verify_pair(self, cookie_token: Optional[str], header_token: Optional[str], expected_subject: Optional[str] = None) -> None:
|
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:
|
if not cookie_token or not header_token:
|
||||||
raise ForbiddenException(message='CSRF token missing')
|
raise ApplicationException(
|
||||||
|
status_code=403,
|
||||||
|
message='CSRF token missing',
|
||||||
|
)
|
||||||
|
|
||||||
if not secrets.compare_digest(cookie_token, header_token):
|
if not secrets.compare_digest(cookie_token, header_token):
|
||||||
raise ForbiddenException(message='CSRF token mismatch')
|
raise ApplicationException(
|
||||||
|
status_code=403,
|
||||||
|
message='CSRF token mismatch',
|
||||||
|
)
|
||||||
|
|
||||||
self.verify(cookie_token, expected_subject=expected_subject)
|
self.verify(cookie_token, expected_subject=expected_subject)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
from jose import jwt, ExpiredSignatureError, JWTError
|
from jose import jwt, ExpiredSignatureError, JWTError
|
||||||
from src.application.contracts import ILogger, IJwtService
|
from src.application.contracts import ILogger, IJwtService
|
||||||
from src.application.domain.dto import AccessTokenPayload
|
from src.application.domain.dto import AccessTokenPayload
|
||||||
from src.application.domain.exceptions import ApplicationException, UnauthorizedException, JwtErrorException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config.settings import settings
|
from src.infrastructure.config.settings import settings
|
||||||
from src.infrastructure.vault import JwtKeyStore
|
from src.infrastructure.vault import JwtKeyStore
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class JwtService(IJwtService):
|
|||||||
|
|
||||||
if payload.get('type') != 'access':
|
if payload.get('type') != 'access':
|
||||||
self._logger.warning(f'Access token invalid type received_type={payload.get('type')}')
|
self._logger.warning(f'Access token invalid type received_type={payload.get('type')}')
|
||||||
raise UnauthorizedException(message='Invalid token type')
|
raise ApplicationException(status_code=401, message='Invalid token type')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return AccessTokenPayload(
|
return AccessTokenPayload(
|
||||||
@@ -32,7 +32,7 @@ class JwtService(IJwtService):
|
|||||||
)
|
)
|
||||||
except KeyError as exception:
|
except KeyError as exception:
|
||||||
self._logger.warning(f'Access token missing claim error={str(exception)}')
|
self._logger.warning(f'Access token missing claim error={str(exception)}')
|
||||||
raise UnauthorizedException(message=f'Missing token claim: {exception}')
|
raise ApplicationException(status_code=401, message=f'Missing token claim: {exception}')
|
||||||
|
|
||||||
async def _decode_and_verify(self, token: str) -> dict:
|
async def _decode_and_verify(self, token: str) -> dict:
|
||||||
kid: str | None = None
|
kid: str | None = None
|
||||||
@@ -42,12 +42,12 @@ class JwtService(IJwtService):
|
|||||||
kid = header.get('kid')
|
kid = header.get('kid')
|
||||||
if not kid:
|
if not kid:
|
||||||
self._logger.warning(f'JWT header missing kid header={header}')
|
self._logger.warning(f'JWT header missing kid header={header}')
|
||||||
raise UnauthorizedException(message='Missing token header: kid')
|
raise ApplicationException(status_code=401, message='Missing token header: kid')
|
||||||
|
|
||||||
received_alg = header.get('alg')
|
received_alg = header.get('alg')
|
||||||
if received_alg != settings.JWT_ALGORITHM:
|
if received_alg != settings.JWT_ALGORITHM:
|
||||||
self._logger.warning(f'JWT invalid algorithm kid={kid} received_alg={received_alg} expected_alg={settings.JWT_ALGORITHM}')
|
self._logger.warning(f'JWT invalid algorithm kid={kid} received_alg={received_alg} expected_alg={settings.JWT_ALGORITHM}')
|
||||||
raise UnauthorizedException(message='Invalid token algorithm')
|
raise ApplicationException(status_code=401, message='Invalid token algorithm')
|
||||||
|
|
||||||
public_pem = await self._key_store.get_public_key_for_kid(str(kid))
|
public_pem = await self._key_store.get_public_key_for_kid(str(kid))
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class JwtService(IJwtService):
|
|||||||
|
|
||||||
if not public_pem:
|
if not public_pem:
|
||||||
self._logger.warning(f'JWT unknown kid kid={kid}')
|
self._logger.warning(f'JWT unknown kid kid={kid}')
|
||||||
raise UnauthorizedException(message='Unknown token kid')
|
raise ApplicationException(status_code=401, message='Unknown token kid')
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
'verify_signature': True,
|
'verify_signature': True,
|
||||||
@@ -85,25 +85,25 @@ class JwtService(IJwtService):
|
|||||||
|
|
||||||
if 'sid' not in payload:
|
if 'sid' not in payload:
|
||||||
self._logger.warning(f'JWT missing sid claim kid={kid}')
|
self._logger.warning(f'JWT missing sid claim kid={kid}')
|
||||||
raise UnauthorizedException(message='Missing token claim: sid')
|
raise ApplicationException(status_code=401, message='Missing token claim: sid')
|
||||||
|
|
||||||
if 'type' not in payload:
|
if 'type' not in payload:
|
||||||
self._logger.warning(f'JWT missing type claim kid={kid}')
|
self._logger.warning(f'JWT missing type claim kid={kid}')
|
||||||
raise UnauthorizedException(message='Missing token claim: type')
|
raise ApplicationException(status_code=401, message='Missing token claim: type')
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
except ExpiredSignatureError as exception:
|
except ExpiredSignatureError as exception:
|
||||||
self._logger.info(f'JWT expired kid={kid} error={str(exception)}')
|
self._logger.info(f'JWT expired kid={kid} error={str(exception)}')
|
||||||
raise UnauthorizedException(message='Token expired')
|
raise ApplicationException(status_code=401, message='Token expired')
|
||||||
|
|
||||||
except ApplicationException:
|
except ApplicationException:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except JWTError as exception:
|
except JWTError as exception:
|
||||||
self._logger.warning(f'JWT decode failed kid={kid} error={str(exception)}')
|
self._logger.warning(f'JWT decode failed kid={kid} error={str(exception)}')
|
||||||
raise UnauthorizedException(message='Invalid token')
|
raise ApplicationException(status_code=401, message='Invalid token')
|
||||||
|
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}')
|
self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}')
|
||||||
raise JwtErrorException(message='JWT decode failed')
|
raise ApplicationException(status_code=500, message='JWT decode failed')
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from src.application.domain.dto import JwtPublicKeySet, JwtPublicKey
|
from src.application.domain.dto import JwtPublicKeySet, JwtPublicKey
|
||||||
from src.application.domain.exceptions import JwtErrorException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.vault.client import VaultClient
|
from src.infrastructure.vault.client import VaultClient
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class JwtKeyStore:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_instance(cls) -> 'JwtKeyStore':
|
def get_instance(cls) -> 'JwtKeyStore':
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
raise JwtErrorException(message='JwtKeyStore not initialized')
|
raise ApplicationException(status_code=500, message='JwtKeyStore not initialized')
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def _read_keyset_sync(self) -> JwtPublicKeySet:
|
def _read_keyset_sync(self) -> JwtPublicKeySet:
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
|||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from fastapi.exceptions import RequestValidationError
|
|
||||||
from src.application.domain.exceptions import ApplicationException, UnauthorizedException
|
from src.application.domain.exceptions import ApplicationException, UnauthorizedException
|
||||||
from src.infrastructure.cache import create_redis_client
|
from src.infrastructure.cache import create_redis_client
|
||||||
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
||||||
@@ -17,9 +15,7 @@ from src.infrastructure.logger import logger
|
|||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.presentation.handlers import (
|
from src.presentation.handlers import (
|
||||||
application_exception_handler,
|
application_exception_handler,
|
||||||
http_exception_handler,
|
|
||||||
unhandled_exception_handler,
|
unhandled_exception_handler,
|
||||||
validation_exception_handler,
|
|
||||||
)
|
)
|
||||||
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
||||||
from src.presentation.routing import me_router
|
from src.presentation.routing import me_router
|
||||||
@@ -84,8 +80,6 @@ app: FastAPI = FastAPI(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
|
||||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
||||||
app.add_exception_handler(ApplicationException, application_exception_handler)
|
app.add_exception_handler(ApplicationException, application_exception_handler)
|
||||||
app.add_exception_handler(Exception, unhandled_exception_handler)
|
app.add_exception_handler(Exception, unhandled_exception_handler)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import inspect
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable, Awaitable, Any, Optional, Annotated
|
from typing import Callable, Awaitable, Any, Optional, Annotated
|
||||||
from fastapi import Request, Header
|
from fastapi import Request, Header
|
||||||
from src.application.domain.exceptions import CsrfErrorException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.security import CsrfService
|
from src.infrastructure.security import CsrfService
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +39,10 @@ def csrf_protect(
|
|||||||
break
|
break
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
raise CsrfErrorException(message='Request is required for CSRF protection')
|
raise ApplicationException(
|
||||||
|
status_code=500,
|
||||||
|
message='Request is required for CSRF protection',
|
||||||
|
)
|
||||||
|
|
||||||
csrf = CsrfService()
|
csrf = CsrfService()
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ from typing import Any, Awaitable, Callable, Literal, Optional, Protocol, runtim
|
|||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from redis.asyncio.client import Redis
|
from redis.asyncio.client import Redis
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.exceptions import (
|
from src.application.domain.exceptions import ApplicationException
|
||||||
RateLimitErrorException,
|
|
||||||
ServiceUnavailableException,
|
|
||||||
TooManyRequestsException,
|
|
||||||
)
|
|
||||||
from src.infrastructure.logger import get_logger
|
from src.infrastructure.logger import get_logger
|
||||||
from src.presentation.dependencies import get_redis
|
from src.presentation.dependencies import get_redis
|
||||||
|
|
||||||
@@ -128,7 +124,7 @@ def rate_limit(
|
|||||||
ident = _call_key_builder(key_builder, request, args, kwargs) # type: ignore[arg-type]
|
ident = _call_key_builder(key_builder, request, args, kwargs) # type: ignore[arg-type]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'RateLimit key_builder failed error={str(e)}')
|
logger.error(f'RateLimit key_builder failed error={str(e)}')
|
||||||
raise RateLimitErrorException(message='Rate limiter key_builder failed')
|
raise ApplicationException(500, 'Rate limiter key_builder failed')
|
||||||
|
|
||||||
route = request.url.path
|
route = request.url.path
|
||||||
method = request.method
|
method = request.method
|
||||||
@@ -157,12 +153,13 @@ def rate_limit(
|
|||||||
logger.warning(f'RateLimit fail-open activated key={redis_key}')
|
logger.warning(f'RateLimit fail-open activated key={redis_key}')
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
raise ServiceUnavailableException(message='Rate limiter unavailable')
|
raise ApplicationException(503, 'Rate limiter unavailable')
|
||||||
|
|
||||||
if count > limit:
|
if count > limit:
|
||||||
retry_after = max(ttl, 0)
|
retry_after = max(ttl, 0)
|
||||||
logger.warning(f'RateLimit exceeded key={redis_key} count={count} limit={limit} retry_after={retry_after}')
|
logger.warning(f'RateLimit exceeded key={redis_key} count={count} limit={limit} retry_after={retry_after}')
|
||||||
raise TooManyRequestsException(
|
raise ApplicationException(
|
||||||
|
status_code=429,
|
||||||
message='Too Many Requests',
|
message='Too Many Requests',
|
||||||
headers={'Retry-After': str(retry_after)},
|
headers={'Retry-After': str(retry_after)},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
from src.presentation.handlers.unhandled_handler import unhandled_exception_handler
|
from src.presentation.handlers.unhandled_handler import unhandled_exception_handler
|
||||||
from src.presentation.handlers.application_handler import application_exception_handler
|
from src.presentation.handlers.application_handler import application_exception_handler
|
||||||
from src.presentation.handlers.http_exception_handler import http_exception_handler
|
|
||||||
from src.presentation.handlers.validation_handler import validation_exception_handler
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
from fastapi import Request
|
|
||||||
from fastapi.responses import ORJSONResponse
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
|
|
||||||
|
|
||||||
async def http_exception_handler(_request: Request, exc: HTTPException) -> ORJSONResponse:
|
|
||||||
return ORJSONResponse(
|
|
||||||
status_code=exc.status_code,
|
|
||||||
content={'detail': exc.detail},
|
|
||||||
headers=dict(exc.headers) if exc.headers else None,
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from fastapi import Request
|
|
||||||
from fastapi.exceptions import RequestValidationError
|
|
||||||
from fastapi.responses import ORJSONResponse
|
|
||||||
|
|
||||||
|
|
||||||
async def validation_exception_handler(_request: Request, exc: RequestValidationError) -> ORJSONResponse:
|
|
||||||
return ORJSONResponse(
|
|
||||||
status_code=422,
|
|
||||||
content={'detail': exc.errors()},
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user