From 2a4e694ca4d87183bb62ed5c4c60035fe1c116b0 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Mon, 19 Jan 2026 19:16:13 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/models/3_20260119185004_update.py | 91 +++++++++++++++++++ src/adapter/postgres.py | 6 ++ src/adapter/telegram_bot.py | 3 + src/controller/http_v1/internal.py | 14 +-- src/domain/login_token.py | 1 + src/usecase/__init__.py | 4 + .../auth/attach_login_token_message.py | 8 ++ src/usecase/auth/validate_login_token.py | 11 +++ tg_bot/backend/backend_client.go | 12 +++ tg_bot/screens/login.go | 8 +- 10 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 migrations/models/3_20260119185004_update.py create mode 100644 src/usecase/auth/attach_login_token_message.py diff --git a/migrations/models/3_20260119185004_update.py b/migrations/models/3_20260119185004_update.py new file mode 100644 index 0000000..37ae6c7 --- /dev/null +++ b/migrations/models/3_20260119185004_update.py @@ -0,0 +1,91 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "login_token" ADD "message_id" INT; + """ + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "login_token" DROP COLUMN "message_id"; + """ + + +MODELS_STATE = ( + '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' +) diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 1f39050..7270ce5 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -227,6 +227,12 @@ class Postgres(DatabaseBase): if updated == 0: raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован + @staticmethod + async def update_login_token_message_id(token: str, message_id: int) -> None: + updated = await domain.LoginToken.filter(token=token).update(message_id=message_id) + if updated == 0: + raise domain.LoginTokenNotFound() + @staticmethod async def get_channel( channel_id: uuid.UUID | None = None, telegram_id: int | None = None, username: str | None = None diff --git a/src/adapter/telegram_bot.py b/src/adapter/telegram_bot.py index e64a298..6698c83 100644 --- a/src/adapter/telegram_bot.py +++ b/src/adapter/telegram_bot.py @@ -22,5 +22,8 @@ class TelegramBot(TelegramBase, TelegramBotWriter): keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) await self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard) + async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None: + await self.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=text, reply_markup=None) + async def edit_message_reply_markup(self, chat_id: int, message_id: int) -> None: await self.bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) diff --git a/src/controller/http_v1/internal.py b/src/controller/http_v1/internal.py index 024d3f3..6872a7c 100644 --- a/src/controller/http_v1/internal.py +++ b/src/controller/http_v1/internal.py @@ -8,19 +8,19 @@ from src import deps, dto internal_router = APIRouter(prefix='/internal', tags=['internal']) -class CreateLoginTokenResponse(BaseModel): - token: str - - @internal_router.post('/auth/login-token') async def create_login_token(input: dto.CreateLoginTokenRequest) -> str: return await deps.get_usecase().create_telegram_login_token(telegram_id=input.telegram_id) -class ValidateTokenRequest(BaseModel): +class AttachLoginTokenMessageRequest(BaseModel): token: str - telegram_id: int - username: str | None = None + message_id: int + + +@internal_router.post('/auth/login-token/message') +async def attach_login_token_message(input: AttachLoginTokenMessageRequest) -> None: + await deps.get_usecase().attach_login_token_message(token=input.token, message_id=input.message_id) @internal_router.get('/auth/jwt') diff --git a/src/domain/login_token.py b/src/domain/login_token.py index 480fa24..e9a0e56 100644 --- a/src/domain/login_token.py +++ b/src/domain/login_token.py @@ -16,6 +16,7 @@ class LoginToken(TimestampedModel): ) expires_at = fields.DatetimeField() used_at = fields.DatetimeField(null=True) + message_id = fields.IntField(null=True) class Meta: table = 'login_token' diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index 7d97105..e68733f 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -15,6 +15,7 @@ from .analytics.get_overview_analytics import get_overview_analytics from .analytics.get_placements_analytics import get_placements_analytics from .analytics.get_projects_analytics import get_projects_analytics from .analytics.get_spending_analytics import get_spending_analytics +from .auth.attach_login_token_message import attach_login_token_message from .auth.create_telegram_login_token import create_telegram_login_token from .auth.get_jwt_by_telegram_id import get_jwt_by_telegram_id from .auth.get_me import get_me @@ -62,6 +63,8 @@ class TelegramBotWriter(typing.Protocol): self, text: str, chat_id: int, buttons: list[list['InlineKeyboardButton']] ) -> None: ... + async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None: ... + async def edit_message_reply_markup(self, chat_id: int, message_id: int) -> None: ... async def create_chat_invite_link(self, chat_id: int, requires_approval: bool = False) -> str: ... @@ -145,6 +148,7 @@ class Usecase: validate_login_token = validate_login_token create_telegram_login_token = create_telegram_login_token + attach_login_token_message = attach_login_token_message get_jwt_by_telegram_id = get_jwt_by_telegram_id get_me = get_me tg_add_project = tg_add_project diff --git a/src/usecase/auth/attach_login_token_message.py b/src/usecase/auth/attach_login_token_message.py new file mode 100644 index 0000000..d6f09ba --- /dev/null +++ b/src/usecase/auth/attach_login_token_message.py @@ -0,0 +1,8 @@ +import typing + +if typing.TYPE_CHECKING: + from .. import Usecase + + +async def attach_login_token_message(self: 'Usecase', token: str, message_id: int) -> None: + await self.database.update_login_token_message_id(token=token, message_id=message_id) diff --git a/src/usecase/auth/validate_login_token.py b/src/usecase/auth/validate_login_token.py index 69d4ee0..3cf0e89 100644 --- a/src/usecase/auth/validate_login_token.py +++ b/src/usecase/auth/validate_login_token.py @@ -1,3 +1,4 @@ +import logging import typing from tortoise import timezone @@ -30,6 +31,16 @@ async def validate_login_token(self: 'Usecase', input: dto.ValidateLoginTokenInp if telegram_user is None: raise domain.UserNotFound(login_token.user_id) + if login_token.message_id is not None: + try: + await self.telegram_bot.edit_message_text( + text='✅ Вы успешно авторизованы', + chat_id=telegram_user.telegram_id, + message_id=login_token.message_id, + ) + except Exception: + logging.getLogger(__name__).exception("Failed to update login message") + access_token = self.jwt_encoder.encode_access_token( user_id=user.id, telegram_id=telegram_user.telegram_id, diff --git a/tg_bot/backend/backend_client.go b/tg_bot/backend/backend_client.go index 5282a4f..caa0f3c 100644 --- a/tg_bot/backend/backend_client.go +++ b/tg_bot/backend/backend_client.go @@ -106,6 +106,18 @@ func (c *Client) CreateLoginToken(ctx context.Context, telegramID int64) (string return token, err } +func (c *Client) AttachLoginTokenMessage(ctx context.Context, token string, messageID int) error { + req := struct { + Token string `json:"token"` + MessageID int `json:"message_id"` + }{ + Token: token, + MessageID: messageID, + } + + return c.do(ctx, http.MethodPost, "api/v1/internal/auth/login-token/message", req, nil) +} + func (c *Client) GetJWTByTelegramID( ctx context.Context, telegramID int64, diff --git a/tg_bot/screens/login.go b/tg_bot/screens/login.go index d2b4c54..d2060ca 100644 --- a/tg_bot/screens/login.go +++ b/tg_bot/screens/login.go @@ -32,7 +32,7 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) { kb := Keyboard(Row(URLButton("Войти на сайт", loginURL))) - _, err = b.SendMessage(fmt.Sprintf(msgHelloLogin, usernameDisplay), b.ChatID, &echotron.MessageOptions{ + res, err := b.SendMessage(fmt.Sprintf(msgHelloLogin, usernameDisplay), b.ChatID, &echotron.MessageOptions{ ReplyMarkup: kb, ParseMode: echotron.HTML, }) @@ -41,6 +41,12 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) { return } + if res.Result != nil { + if err := b.Backend.AttachLoginTokenMessage(context.Background(), token, res.Result.ID); err != nil { + log.Err(err).Msg("AttachLoginTokenMessage error") + } + } + b.SetState(&MainMenu{}, bot.NewMessage) } func (s *Login) HandleCallback(b *bot.Bot, u *echotron.Update) {}