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