121 lines
4.0 KiB
Python
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,
|
|
) |