Files
auth/src/application/commands/user_registration_complete.py
2026-04-12 09:16:16 +03:00

121 lines
4.0 KiB
Python

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