Initial commit

This commit is contained in:
2026-04-12 09:16:16 +03:00
commit 5fe8efc5d4
98 changed files with 5351 additions and 0 deletions

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