редактирование сообщения авторизации

This commit is contained in:
Artem Tsyrulnikov
2026-01-19 19:16:13 +03:00
parent a90c8b7df6
commit 2a4e694ca4
10 changed files with 150 additions and 8 deletions

View File

@@ -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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'
)

View File

@@ -227,6 +227,12 @@ class Postgres(DatabaseBase):
if updated == 0: if updated == 0:
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован 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 @staticmethod
async def get_channel( async def get_channel(
channel_id: uuid.UUID | None = None, telegram_id: int | None = None, username: str | None = None channel_id: uuid.UUID | None = None, telegram_id: int | None = None, username: str | None = None

View File

@@ -22,5 +22,8 @@ class TelegramBot(TelegramBase, TelegramBotWriter):
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard) 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: 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) await self.bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None)

View File

@@ -8,19 +8,19 @@ from src import deps, dto
internal_router = APIRouter(prefix='/internal', tags=['internal']) internal_router = APIRouter(prefix='/internal', tags=['internal'])
class CreateLoginTokenResponse(BaseModel):
token: str
@internal_router.post('/auth/login-token') @internal_router.post('/auth/login-token')
async def create_login_token(input: dto.CreateLoginTokenRequest) -> str: async def create_login_token(input: dto.CreateLoginTokenRequest) -> str:
return await deps.get_usecase().create_telegram_login_token(telegram_id=input.telegram_id) return await deps.get_usecase().create_telegram_login_token(telegram_id=input.telegram_id)
class ValidateTokenRequest(BaseModel): class AttachLoginTokenMessageRequest(BaseModel):
token: str token: str
telegram_id: int message_id: int
username: str | None = None
@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') @internal_router.get('/auth/jwt')

View File

@@ -16,6 +16,7 @@ class LoginToken(TimestampedModel):
) )
expires_at = fields.DatetimeField() expires_at = fields.DatetimeField()
used_at = fields.DatetimeField(null=True) used_at = fields.DatetimeField(null=True)
message_id = fields.IntField(null=True)
class Meta: class Meta:
table = 'login_token' table = 'login_token'

View File

@@ -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_placements_analytics import get_placements_analytics
from .analytics.get_projects_analytics import get_projects_analytics from .analytics.get_projects_analytics import get_projects_analytics
from .analytics.get_spending_analytics import get_spending_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.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_jwt_by_telegram_id import get_jwt_by_telegram_id
from .auth.get_me import get_me 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']] self, text: str, chat_id: int, buttons: list[list['InlineKeyboardButton']]
) -> None: ... ) -> 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 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: ... 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 validate_login_token = validate_login_token
create_telegram_login_token = create_telegram_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_jwt_by_telegram_id = get_jwt_by_telegram_id
get_me = get_me get_me = get_me
tg_add_project = tg_add_project tg_add_project = tg_add_project

View File

@@ -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)

View File

@@ -1,3 +1,4 @@
import logging
import typing import typing
from tortoise import timezone from tortoise import timezone
@@ -30,6 +31,16 @@ async def validate_login_token(self: 'Usecase', input: dto.ValidateLoginTokenInp
if telegram_user is None: if telegram_user is None:
raise domain.UserNotFound(login_token.user_id) 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( access_token = self.jwt_encoder.encode_access_token(
user_id=user.id, user_id=user.id,
telegram_id=telegram_user.telegram_id, telegram_id=telegram_user.telegram_id,

View File

@@ -106,6 +106,18 @@ func (c *Client) CreateLoginToken(ctx context.Context, telegramID int64) (string
return token, err 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( func (c *Client) GetJWTByTelegramID(
ctx context.Context, ctx context.Context,
telegramID int64, telegramID int64,

View File

@@ -32,7 +32,7 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) {
kb := Keyboard(Row(URLButton("Войти на сайт", loginURL))) 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, ReplyMarkup: kb,
ParseMode: echotron.HTML, ParseMode: echotron.HTML,
}) })
@@ -41,6 +41,12 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) {
return 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) b.SetState(&MainMenu{}, bot.NewMessage)
} }
func (s *Login) HandleCallback(b *bot.Bot, u *echotron.Update) {} func (s *Login) HandleCallback(b *bot.Bot, u *echotron.Update) {}