from datetime import timedelta, datetime, timezone from ulid import ULID from src.application.abstractions import IUnitOfWork from src.application.contracts import IHashService, IJwtService, ILogger, ICache from src.application.domain.dto import UserCreatedDto from src.application.domain.entities import UserEntity from src.application.domain.exceptions import ApplicationException from src.infrastructure.config import settings from src.infrastructure.database.decorators import transactional class UserRegistrationCompleteCommand: def __init__( self, unit_of_work: IUnitOfWork, hash_service: IHashService, jwt_service: IJwtService, cache: ICache, logger: ILogger, ): self._unit_of_work = unit_of_work self._cache = cache self._hash_service = hash_service self._jwt_service = jwt_service self._logger = logger @transactional async def __call__( self, *, email: str, password: str, device_id: str, code: str, user_agent: str | None, ip: str | None, ) -> UserCreatedDto: email = (email or '').strip().lower() code = (code or '').strip() code_key = f'reg:code:{code}' email_key = f'reg:email:{email}' cached_email = await self._cache.get(code_key) if not cached_email: self._logger.info(f'Registration failed: code not found (email={email}, code={code})') raise ApplicationException(400, 'Invalid or expired code') if cached_email != email: self._logger.info(f'Registration failed: code-email mismatch (email={email}, cached_email={cached_email})') raise ApplicationException(400, 'Invalid or expired code') code_hash = await self._cache.get(email_key) if not code_hash: self._logger.info(f'Registration failed: email key missing (email={email})') 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(f'Registration failed: code hash mismatch (email={email})') raise ApplicationException(400, 'Invalid or expired code') deleted_code = await self._cache.delete(code_key) deleted_email = await self._cache.delete(email_key) if not deleted_code or not deleted_email: self._logger.info( f'Registration cleanup: keys already missing ' f'(email={email}, deleted_code={deleted_code}, deleted_email={deleted_email})' ) now = datetime.now(timezone.utc) password_hash = await self._hash_service.hash(value=password) user: UserEntity = await self._unit_of_work.user_repository.create_user( email=email, password_hash=password_hash, ) sid = str(ULID()) jti = str(ULID()) refresh_jti_hash = await self._hash_service.hash(value=jti) refresh_expires_at = now + timedelta(seconds=int(settings.JWT_REFRESH_TTL_SECONDS)) await self._unit_of_work.session_repository.upsert_by_device( user_id=user.id, device_id=device_id, sid=sid, refresh_jti_hash=refresh_jti_hash, refresh_expires_at=refresh_expires_at, user_agent=user_agent, ip=ip, now=now, ) access_token = await self._jwt_service.create_access_token( user_id=user.id, sid=sid, ) refresh_token = await ( self._jwt_service.create_refresh_token( user_id=user.id, sid=sid, refresh_jti=jti, )) self._logger.info(f'User registered successfully user_id={user.id} device_id={device_id} sid={sid}') return UserCreatedDto( id=user.id, email=user.email, access_token=access_token, refresh_token=refresh_token, )