115 lines
4.2 KiB
Python
115 lines
4.2 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 UserLoginDto
|
|
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 UserLoginCompleteCommand:
|
|
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._hash_service = hash_service
|
|
self._jwt_service = jwt_service
|
|
self._cache = cache
|
|
self._logger = logger
|
|
|
|
@transactional
|
|
async def __call__(
|
|
self,
|
|
*,
|
|
email: str,
|
|
password: str,
|
|
code: str,
|
|
device_id: str,
|
|
user_agent: str | None,
|
|
ip: str | None,
|
|
) -> UserLoginDto:
|
|
email = (email or '').strip().lower()
|
|
code = (code or '').strip()
|
|
|
|
code_key = f'login:code:{code}'
|
|
email_key = f'login:email:{email}'
|
|
|
|
cached_email = await self._cache.get(code_key)
|
|
if not cached_email:
|
|
self._logger.info(f'Login failed: code not found (email={email})')
|
|
raise ApplicationException(400, 'Invalid or expired code')
|
|
|
|
if cached_email != email:
|
|
self._logger.info(f'Login 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'Login 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'Login failed: code hash mismatch (email={email})')
|
|
raise ApplicationException(400, 'Invalid or expired code')
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
user: UserEntity = await self._unit_of_work.user_repository.get_user_by_email(email=email)
|
|
|
|
ok = await self._hash_service.verify(plain_value=password, hashed_value=user.password_hash)
|
|
if not ok:
|
|
self._logger.warning(f'{user.id} login failed: invalid credentials')
|
|
raise ApplicationException(status_code=401, message='Invalid credentials')
|
|
|
|
try:
|
|
await self._cache.delete(code_key)
|
|
await self._cache.delete(email_key)
|
|
except Exception as e:
|
|
self._logger.warning(f'Login cleanup failed (email={email}): {e}')
|
|
|
|
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)
|
|
|
|
return UserLoginDto(
|
|
id=user.id,
|
|
email=user.email,
|
|
first_name=user.first_name,
|
|
middle_name=user.middle_name,
|
|
last_name=user.last_name,
|
|
birth_date=user.birth_date,
|
|
crypto_wallet=user.crypto_wallet,
|
|
phone=user.phone,
|
|
passport_data=user.passport_data,
|
|
inn=user.inn,
|
|
kyc_verified=user.kyc_verified,
|
|
kyc_verified_at=user.kyc_verified_at,
|
|
created_at=user.created_at,
|
|
updated_at=user.updated_at,
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
) |