feat: creatives and external channels

This commit is contained in:
Artem Tsyrulnikov
2025-11-10 15:13:38 +03:00
parent 3800c72662
commit cd167fdb43
73 changed files with 3024 additions and 947 deletions

123
src/usecase/__init__.py Normal file
View File

@@ -0,0 +1,123 @@
import datetime
import typing
from dataclasses import dataclass
from uuid import UUID
from src import domain
from .creative.archive_creative import archive_creative
from .creative.create_creative import create_creative
from .creative.delete_creative import delete_creative
from .creative.get_creative import get_creative
from .creative.get_creatives import get_creatives
from .creative.update_creative import update_creative
from .external_channel.create_external_channel import create_external_channel
from .external_channel.delete_external_channel import delete_external_channel
from .external_channel.get_external_channels import get_external_channels
from .external_channel.import_external_channels_from_excel import import_external_channels_from_excel
from .external_channel.update_external_channel_links import update_external_channel_links
from .target_channel.connect_target_chan import connect_target_chan
from .target_channel.disconnect_target_chan import disconnect_target_chan
from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_chan_by_tg_id
from .target_channel.get_user_target_chans import get_user_target_chans
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
from .telegram_login import telegram_login
from .validate_login_token import validate_login_token
class Database(typing.Protocol):
def transaction(self) -> typing.AsyncContextManager[None]: ...
async def get_user(self, *, user_id: UUID | None = None, telegram_id: int | None = None) -> domain.User | None: ...
async def create_user(self, user: domain.User) -> domain.User: ...
async def create_login_token(
self, user_id: UUID, token: str, expires_at: datetime.datetime
) -> domain.LoginToken: ...
async def get_login_token(self, token: str) -> domain.LoginToken | None: ...
async def mark_token_as_used(self, token: str) -> None: ...
async def get_target_channel(
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
) -> domain.TargetChannel | None: ...
async def create_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel: ...
async def upsert_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel: ...
async def get_user_target_channels(self, user_id: UUID) -> list[domain.TargetChannel]: ...
async def update_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel: ...
async def get_external_channel(
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
) -> domain.ExternalChannel | None: ...
async def create_external_channel(self, channel: domain.ExternalChannel) -> domain.ExternalChannel: ...
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
async def add_external_channel_to_targets(
self, external_channel_id: UUID, target_channel_ids: list[UUID]
) -> None: ...
async def remove_external_channel_from_target(self, external_channel_id: UUID, target_channel_id: UUID) -> None: ...
async def delete_external_channel(self, channel_id: UUID) -> None: ...
async def get_creative(self, user_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
async def create_creative(self, creative: domain.Creative) -> domain.Creative: ...
async def update_creative(self, creative: domain.Creative) -> domain.Creative: ...
async def get_user_creatives(
self,
user_id: UUID,
*,
target_channel_id: UUID | None = None,
include_archived: bool = False,
) -> list[domain.Creative]: ...
async def delete_creative(self, creative_id: UUID) -> None: ...
class TelegramWriter(typing.Protocol):
async def send_message(self, text: str, chat_id: int) -> None: ...
async def send_message_with_button(self, text: str, chat_id: int, button_text: str, button_url: str) -> None: ...
async def get_chat_member(self, chat_id: int, user_id: int) -> dict[str, typing.Any]: ...
class JWTEncoder(typing.Protocol):
def encode_access_token(self, user_id: UUID, telegram_id: int, username: str | None = None) -> str: ...
@dataclass
class Usecase:
database: Database
telegram_writer: TelegramWriter
jwt_encoder: JWTEncoder
validate_login_token = validate_login_token
telegram_login = telegram_login
connect_target_chan = connect_target_chan
get_user_target_chans = get_user_target_chans
disconnect_target_chan = disconnect_target_chan
disconnect_target_chan_by_tg_id = disconnect_target_chan_by_tg_id
update_target_chan_permissions = update_target_chan_permissions
get_external_channels = get_external_channels
create_external_channel = create_external_channel
delete_external_channel = delete_external_channel
update_external_channel_links = update_external_channel_links
import_external_channels_from_excel = import_external_channels_from_excel
get_creatives = get_creatives
get_creative = get_creative
create_creative = create_creative
update_creative = update_creative
archive_creative = archive_creative
delete_creative = delete_creative

View File

@@ -0,0 +1,32 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def archive_creative(self: 'Usecase', creative_id: uuid.UUID, user_id: uuid.UUID) -> dto.CreativeOutput:
async with self.database.transaction():
creative = await self.database.get_creative(user_id, creative_id)
if not creative:
log.warning('User %s attempted to change archive status for unavailable creative %s', user_id, creative_id)
raise domain.CreativeNotFound(creative_id)
creative.status = domain.CreativeStatus.ARCHIVED
updated = await self.database.update_creative(creative)
return dto.CreativeOutput(
id=updated.id,
name=updated.name,
text=updated.text,
target_channel_id=updated.target_channel_id,
target_channel_title=updated.target_channel.title,
created_at=updated.created_at,
status=updated.status,
purchases_count=updated.purchases_count,
)

View File

@@ -0,0 +1,40 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID) -> dto.CreativeOutput:
async with self.database.transaction():
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
if not target_channel:
log.warning(
'User %s attempted to create creative for unavailable target %s', user_id, input.target_channel_id
)
raise domain.TargetChannelNotFound(input.target_channel_id)
creative = domain.Creative(
name=input.name,
text=input.text,
target_channel_id=target_channel.id,
user_id=user_id,
)
created = await self.database.create_creative(creative)
return dto.CreativeOutput(
id=created.id,
name=created.name,
text=created.text,
target_channel_id=created.target_channel_id,
target_channel_title=created.target_channel.title,
created_at=created.created_at,
status=created.status,
purchases_count=created.purchases_count,
)

View File

@@ -0,0 +1,23 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> None:
async with self.database.transaction():
creative = await self.database.get_creative(input.user_id, input.creative_id)
if not creative:
log.warning('User %s attempted to delete unavailable creative %s', input.user_id, input.creative_id)
raise domain.CreativeNotFound(input.creative_id)
if creative.purchases_count > 0:
log.warning('Creative %s is used in purchases and cannot be deleted', input.creative_id)
raise domain.CreativeInUse(input.creative_id)
await self.database.delete_creative(input.creative_id)

View File

@@ -0,0 +1,28 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.CreativeOutput:
async with self.database.transaction():
creative = await self.database.get_creative(input.user_id, input.creative_id)
if not creative:
log.warning('User %s attempted to access unavailable creative %s', input.user_id, input.creative_id)
raise domain.CreativeNotFound(input.creative_id)
return dto.CreativeOutput(
id=creative.id,
name=creative.name,
text=creative.text,
target_channel_id=creative.target_channel_id,
target_channel_title=creative.target_channel.title,
created_at=creative.created_at,
status=creative.status,
purchases_count=creative.purchases_count,
)

View File

@@ -0,0 +1,29 @@
from typing import TYPE_CHECKING
from src import dto
if TYPE_CHECKING:
from .. import Usecase
async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.GetCreativesOutput:
async with self.database.transaction():
creatives = await self.database.get_user_creatives(
input.user_id, target_channel_id=input.target_channel_id, include_archived=input.include_archived
)
return dto.GetCreativesOutput(
creatives=[
dto.CreativeOutput(
id=creative.id,
name=creative.name,
text=creative.text,
target_channel_id=creative.target_channel_id,
target_channel_title=creative.target_channel.title,
created_at=creative.created_at,
status=creative.status,
purchases_count=creative.purchases_count,
)
for creative in creatives
]
)

View File

@@ -0,0 +1,38 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_creative(
self: 'Usecase', creative_id: uuid.UUID, input: dto.UpdateCreativeInput, user_id: uuid.UUID
) -> dto.CreativeOutput:
async with self.database.transaction():
creative = await self.database.get_creative(user_id, creative_id)
if not creative:
log.warning('User %s attempted to update unavailable creative %s', user_id, creative_id)
raise domain.CreativeNotFound(creative_id)
if input.name is not None:
creative.name = input.name
if input.text is not None:
creative.text = input.text
updated = await self.database.update_creative(creative)
return dto.CreativeOutput(
id=updated.id,
name=updated.name,
text=updated.text,
target_channel_id=updated.target_channel_id,
target_channel_title=updated.target_channel.title,
created_at=updated.created_at,
status=updated.status,
purchases_count=updated.purchases_count,
)

View File

@@ -0,0 +1,47 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def create_external_channel(
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID
) -> dto.ExternalChannelOutput:
async with self.database.transaction():
for target_id in input.target_channel_ids:
target_channel = await self.database.get_target_channel(user_id, channel_id=target_id)
if not target_channel:
log.warning('User %s attempted to add external channel to unavailable target %s', user_id, target_id)
raise domain.TargetChannelNotFound(target_id)
channel = domain.ExternalChannel(
telegram_id=input.telegram_id,
title=input.title,
username=input.username,
description=input.description,
subscribers_count=input.subscribers_count,
user_id=user_id,
)
created_channel = await self.database.create_external_channel(channel)
if input.target_channel_ids:
await self.database.add_external_channel_to_targets(created_channel.id, input.target_channel_ids)
log.info(
'External channel %s created and linked to %s target channels', input.telegram_id, len(input.target_channel_ids)
)
return dto.ExternalChannelOutput(
id=created_channel.id,
telegram_id=created_channel.telegram_id,
title=created_channel.title,
username=created_channel.username,
description=created_channel.description,
subscribers_count=created_channel.subscribers_count,
)

View File

@@ -0,0 +1,20 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def delete_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
async with self.database.transaction():
channel = await self.database.get_external_channel(input.user_id, channel_id=input.channel_id)
if not channel:
log.warning('User %s attempted to delete unavailable external channel %s', input.user_id, input.channel_id)
raise domain.ExternalChannelNotFound(input.channel_id)
await self.database.delete_external_channel(input.channel_id)
log.info('User %s deleted external channel %s', input.user_id, input.channel_id)

View File

@@ -0,0 +1,35 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
async with self.database.transaction():
target_channel = await self.database.get_target_channel(input.user_id, channel_id=input.target_channel_id)
if not target_channel:
log.warning(
'Target channel %s not found or not accessible for user %s', input.target_channel_id, input.user_id
)
raise domain.TargetChannelNotFound(input.target_channel_id)
channels = await self.database.get_external_channels_for_target(input.target_channel_id)
return dto.GetExternalChannelsOutput(
external_channels=[
dto.ExternalChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
description=channel.description,
subscribers_count=channel.subscribers_count,
)
for channel in channels
]
)

View File

@@ -0,0 +1,41 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def import_external_channels_from_excel(
self: 'Usecase', input: dto.ImportExternalChannelsInput
) -> dto.ImportExternalChannelsOutput:
async with self.database.transaction():
target_channel = await self.database.get_target_channel(
user_id=input.user_id, channel_id=input.target_channel_id
)
if not target_channel:
log.warning(
'User %s attempted to import to unavailable target channel %s', input.user_id, input.target_channel_id
)
raise domain.TargetChannelNotFound(input.target_channel_id)
created_count = 0
skipped_count = 0
errors: list[str] = []
log.info(
'Excel import completed for target %s: %s created, %s skipped, %s errors',
input.target_channel_id,
created_count,
skipped_count,
len(errors),
)
return dto.ImportExternalChannelsOutput(
created_count=created_count,
skipped_count=skipped_count,
errors=errors,
)

View File

@@ -0,0 +1,50 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_external_channel_links(
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelLinksInput, user_id: uuid.UUID
) -> dto.ExternalChannelOutput:
async with self.database.transaction():
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
if not external_channel:
log.warning('External channel %s not found', channel_id)
raise domain.ExternalChannelNotFound(channel_id)
for target_id in input.add_target_channel_ids:
target_channel = await self.database.get_target_channel(user_id, channel_id=target_id)
if not target_channel:
log.warning(
'User %s attempted to link external channel to target %s they do not own', user_id, target_id
)
raise domain.TargetChannelNotFound(target_id)
if input.add_target_channel_ids:
await self.database.add_external_channel_to_targets(channel_id, input.add_target_channel_ids)
for target_id in input.remove_target_channel_ids:
await self.database.remove_external_channel_from_target(channel_id, target_id)
log.info(
'External channel %s links updated: %s added, %s removed',
channel_id,
len(input.add_target_channel_ids),
len(input.remove_target_channel_ids),
)
return dto.ExternalChannelOutput(
id=external_channel.id,
telegram_id=external_channel.telegram_id,
title=external_channel.title,
username=external_channel.username,
description=external_channel.description,
subscribers_count=external_channel.subscribers_count,
)

View File

@@ -1,18 +0,0 @@
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from src.usecase.usecase import Usecase
async def get_user(self: 'Usecase', user_id: uuid.UUID) -> dto.GetUserOutput:
user: domain.User | None = await self.database.get_user(user_id=user_id)
if user is None:
raise domain.UserNotFound(user_id)
return dto.GetUserOutput(
user_id=user.id,
name=user.name,
)

View File

@@ -1,22 +0,0 @@
import uuid
from typing import TYPE_CHECKING
from src import domain
if TYPE_CHECKING:
from src.usecase.usecase import Usecase
async def notify_user(self: 'Usecase', user_id: uuid.UUID, chat_id: int, message: str) -> None:
"""Send notification to user via Telegram"""
if self.telegram_writer is None:
raise RuntimeError('TelegramWriter not configured')
# Get user from database
user = await self.database.get_user(user_id)
if user is None:
raise domain.UserNotFound(user_id)
# Send personalized message
text = f'Привет, {user.name}!\n\n{message}'
await self.telegram_writer.send_message(chat_id=chat_id, text=text)

View File

@@ -0,0 +1,77 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def connect_target_chan(self: 'Usecase', input: dto.ConnectTargetChanInput) -> dto.ConnectTargetChanOutput:
permissions = input.bot_permissions
if not permissions.is_admin:
log.warning(f'Bot is not admin in channel {input.telegram_id}. Attempted by user {input.user_telegram_id}')
await self.telegram_writer.send_message(
f'⚠️ Бот был добавлен в канал "{input.title}", но не является админом.\n\n'
'Пожалуйста, сделайте бота администратором канала.',
chat_id=input.user_telegram_id,
)
raise domain.ChannelNoAdminRights()
missing_permissions = []
if not permissions.can_invite_users:
missing_permissions.append('Создание инвайт-ссылок')
if not permissions.can_restrict_members:
missing_permissions.append('Управление пользователями (видеть вступления)')
if missing_permissions:
log.warning(
f'Bot lacks required permissions in channel {input.telegram_id}: {missing_permissions}. '
f'Attempted by user {input.user_telegram_id}'
)
permissions_text = '\n'.join(f'{p}' for p in missing_permissions)
await self.telegram_writer.send_message(
f'⚠️ Бот был добавлен в канал "{input.title}", но не имеет необходимых прав.\n\n'
f'Отсутствующие права:\n{permissions_text}\n\n'
'Пожалуйста, предоставьте эти права боту в настройках канала.',
chat_id=input.user_telegram_id,
)
raise domain.ChannelNoAdminRights()
async with self.database.transaction():
user = await self.database.get_user(telegram_id=input.user_telegram_id)
if not user:
log.warning(f'User {input.user_telegram_id} not found when trying to connect channel {input.telegram_id}')
await self.telegram_writer.send_message(
f'⚠️ Канал "{input.title}" не может быть подключен.\n\n'
'Вы должны сначала авторизоваться в веб-панели перед подключением каналов.',
chat_id=input.user_telegram_id,
)
raise domain.UserNotFound()
channel = domain.TargetChannel(
telegram_id=input.telegram_id,
title=input.title,
username=input.username,
user_id=user.id,
is_active=True,
)
upserted_channel = await self.database.upsert_target_channel(channel)
log.info(f'Channel {input.telegram_id} connected/updated successfully by user {input.user_telegram_id}')
await self.telegram_writer.send_message(
f'✅ Канал "{input.title}" успешно подключен!',
chat_id=input.user_telegram_id,
)
return dto.ConnectTargetChanOutput(
id=upserted_channel.id,
telegram_id=upserted_channel.telegram_id,
title=upserted_channel.title,
username=upserted_channel.username,
is_active=upserted_channel.is_active,
)

View File

@@ -0,0 +1,21 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def disconnect_target_chan(self: 'Usecase', input: dto.DisconnectTargetChanInput) -> None:
async with self.database.transaction():
channel = await self.database.get_target_channel(channel_id=input.channel_id, user_id=input.user_id)
if not channel:
raise domain.TargetChannelNotFound(input.channel_id)
channel.is_active = False
await self.database.update_target_channel(channel)
log.info(f'Target channel {input.channel_id} deactivated')

View File

@@ -0,0 +1,22 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.DisconnectTargetChanByTgIdInput) -> None:
async with self.database.transaction():
channel = await self.database.get_target_channel(input.user_id, telegram_id=input.telegram_id)
if not channel:
log.warning(f'Target channel {input.telegram_id} not found')
raise domain.TargetChannelNotFound()
channel.is_active = False
await self.database.update_target_channel(channel)
log.info(f'Target channel {input.telegram_id} deactivated')

View File

@@ -0,0 +1,24 @@
from typing import TYPE_CHECKING
from src import dto
if TYPE_CHECKING:
from .. import Usecase
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
async with self.database.transaction():
channels = await self.database.get_user_target_channels(input.user_id)
return dto.GetUserTargetChansOutput(
target_channels=[
dto.ConnectTargetChanOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
is_active=channel.is_active,
)
for channel in channels
]
)

View File

@@ -0,0 +1,49 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTargetChanPermissionsInput) -> None:
missing_permissions = []
if not input.permissions.can_invite_users:
missing_permissions.append('Создание инвайт-ссылок')
if not input.permissions.can_restrict_members:
missing_permissions.append('Управление пользователями')
async with self.database.transaction():
channel = await self.database.get_target_channel(user_id, telegram_id=input.telegram_id)
if not channel:
log.warning(f'Target channel {input.telegram_id} not found when permissions changed')
raise domain.TargetChannelNotFound()
if not missing_permissions:
if not channel.is_active:
channel.is_active = True
await self.database.update_target_channel(channel)
await self.telegram_writer.send_message(
f'✅ Канал "{input.chat_title}" был активирован.\n\nВсе необходимые права боту предоставлены!',
chat_id=input.user_telegram_id,
)
log.info(f'Target channel {input.telegram_id} reactivated - all permissions granted')
return
if channel.is_active:
channel.is_active = False
await self.database.update_target_channel(channel)
missed_permissions = '\n'.join(f'{p}' for p in missing_permissions)
await self.telegram_writer.send_message(
f'⚠️ Канал "{input.chat_title}" был деактивирован.\
\n\nБоту убрали необходимые права:\n{missed_permissions}',
chat_id=input.user_telegram_id,
)
log.warning(
f'Target channel {input.telegram_id} deactivated due to missing permissions: {missing_permissions}'
)

View File

@@ -0,0 +1,36 @@
import datetime
import secrets
import typing
from src import domain, dto
from src.config import settings
if typing.TYPE_CHECKING:
from . import Usecase
async def telegram_login(self: 'Usecase', input: dto.TelegramLoginInput) -> None:
async with self.database.transaction():
user: domain.User | None = await self.database.get_user(telegram_id=input.telegram_id)
if not user:
user = domain.User(
telegram_id=input.telegram_id,
username=input.username,
)
user = await self.database.create_user(user)
token = secrets.token_urlsafe(32)
expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=10)
await self.database.create_login_token(user.id, token, expires_at)
login_url = f'{settings.app.URL}/auth/complete?token={token}'
username_display = f'@{user.username}' if user.username else 'пользователь'
await self.telegram_writer.send_message_with_button(
f'Привет, {username_display}!\n\nНажми кнопку ниже для входа на сайт.',
chat_id=input.chat_id,
button_text='Войти на сайт',
button_url=login_url,
)

View File

@@ -1,25 +0,0 @@
import uuid
from dataclasses import dataclass
from typing import Protocol
from src import domain
from .get_user import get_user
from .notify_user import notify_user
class Database(Protocol):
async def get_user(self, user_id: uuid.UUID) -> domain.User | None: ...
class TelegramWriter(Protocol):
async def send_message(self, chat_id: int, text: str) -> None: ...
@dataclass
class Usecase:
database: Database
telegram_writer: TelegramWriter
get_user = get_user
notify_user = notify_user

View File

@@ -0,0 +1,38 @@
import datetime
import typing
from src import domain, dto
if typing.TYPE_CHECKING:
from . import Usecase
async def validate_login_token(self: 'Usecase', input: dto.ValidateLoginTokenInput) -> dto.ValidateLoginTokenOutput:
async with self.database.transaction():
login_token = await self.database.get_login_token(input.token)
if not login_token:
raise domain.LoginTokenNotFound()
if login_token.used_at:
raise domain.LoginTokenAlreadyUsed()
if login_token.expires_at < datetime.datetime.now(datetime.UTC):
raise domain.LoginTokenExpired()
user = await self.database.get_user(user_id=login_token.user_id)
if not user:
raise domain.UserNotFound(login_token.user_id)
await self.database.mark_token_as_used(input.token)
# Генерируем JWT токен с информацией о пользователе
access_token = self.jwt_encoder.encode_access_token(
user_id=user.id,
telegram_id=user.telegram_id,
username=user.username,
)
return dto.ValidateLoginTokenOutput(
access_token=access_token,
)