84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
from src.application.abstractions import IUnitOfWork
|
|
from src.application.contracts import IHashService, ILogger, ICache
|
|
from src.application.domain.exceptions import ApplicationException
|
|
from src.infrastructure.database.decorators import transactional
|
|
|
|
|
|
class ForgotPasswordCompleteCommand:
|
|
def __init__(
|
|
self,
|
|
unit_of_work: IUnitOfWork,
|
|
hash_service: IHashService,
|
|
cache: ICache,
|
|
logger: ILogger,
|
|
):
|
|
self._unit_of_work = unit_of_work
|
|
self._hash_service = hash_service
|
|
self._cache = cache
|
|
self._logger = logger
|
|
|
|
@staticmethod
|
|
def _normalize_email(email: str) -> str:
|
|
return email.strip().lower()
|
|
|
|
@transactional
|
|
async def __call__(
|
|
self,
|
|
*,
|
|
email: str,
|
|
code: str,
|
|
new_password: str,
|
|
confirm_password: str,
|
|
) -> bool:
|
|
code = (code or '').strip()
|
|
normalized = self._normalize_email(email)
|
|
|
|
EMAIL_PREFIX = 'forgot_password:email:'
|
|
CODE_PREFIX = 'forgot_password:code:'
|
|
|
|
if new_password != confirm_password:
|
|
self._logger.info('Forgot password failed: passwords do not match')
|
|
raise ApplicationException(400, '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')
|
|
|
|
if cached_email != normalized:
|
|
self._logger.info('Forgot password failed: code-email mismatch')
|
|
raise ApplicationException(400, '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')
|
|
|
|
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')
|
|
|
|
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')
|
|
|
|
new_password_hash = await self._hash_service.hash(new_password)
|
|
user = await self._unit_of_work.user_repository.set_password(
|
|
user_id=user.id,
|
|
password_hash=new_password_hash,
|
|
)
|
|
await self._cache.set_user(user.id, user)
|
|
|
|
try:
|
|
await self._cache.delete(code_key)
|
|
await self._cache.delete(email_key)
|
|
except Exception as e:
|
|
self._logger.warning(f'Forgot password cleanup failed (user_id={user.id}): {e}')
|
|
|
|
self._logger.info(f'Password reset via forgot flow for user_id={user.id}')
|
|
return True
|