Initial commit
This commit is contained in:
117
src/application/commands/user_login_complete.py
Normal file
117
src/application/commands/user_login_complete.py
Normal file
@@ -0,0 +1,117 @@
|
||||
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,
|
||||
bik=user.bik,
|
||||
account_number=user.account_number,
|
||||
card_number=user.card_number,
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user