diff --git a/src/auth/telegram-bot-login.ts b/src/auth/telegram-bot-login.ts index fa13621..07b8b88 100644 --- a/src/auth/telegram-bot-login.ts +++ b/src/auth/telegram-bot-login.ts @@ -38,7 +38,7 @@ type TelegramSentMessage = { }; const loginPrefix = 'login_'; -const loginTimerHandles = new Map>(); +const loginExpirationHandles = new Map>(); function randomToken() { return randomBytes(32).toString('base64url'); @@ -124,12 +124,12 @@ function formatRemaining(expiresAt: Date) { return `${minutes}:${rest}`; } -function loginReplyMarkup(token: string, expiresAt: Date) { +function loginReplyMarkup(token: string) { return { inline_keyboard: [ [ { - text: 'Открыть MapFlow', + text: 'Войти', url: `${config.webAppUrl}?telegram_login=${encodeURIComponent(token)}`, }, ], @@ -138,12 +138,7 @@ function loginReplyMarkup(token: string, expiresAt: Date) { } function loginMessageText(expiresAt: Date) { - return [ - 'Вход в MapFlow', - '', - 'Перейдите по ссылке ниже.', - `Ссылка активна: ${formatRemaining(expiresAt)}`, - ].join('\n'); + return [formatRemaining(expiresAt), '', 'Войти в MapFlow'].join('\n'); } async function sendLoginMessage( @@ -152,7 +147,7 @@ async function sendLoginMessage( token?: string, expiresAt?: Date, ) { - const replyMarkup = token && expiresAt ? loginReplyMarkup(token, expiresAt) : undefined; + const replyMarkup = token && expiresAt ? loginReplyMarkup(token) : undefined; return callTelegram('sendMessage', { chat_id: chatId, @@ -165,67 +160,79 @@ async function editLoginMessage( chatId: string, messageId: number, text: string, - token?: string, - expiresAt?: Date, ) { - const replyMarkup = token && expiresAt - ? loginReplyMarkup(token, expiresAt) - : { inline_keyboard: [] }; await callTelegram('editMessageText', { chat_id: chatId, message_id: messageId, text, - reply_markup: replyMarkup, + reply_markup: { inline_keyboard: [] }, }); } -function scheduleLoginMessageTimer( +function scheduleLoginExpiration( requestId: string, - token: string, chatId: string, messageId: number, expiresAt: Date, ) { - const existingHandle = loginTimerHandles.get(requestId); + const existingHandle = loginExpirationHandles.get(requestId); if (existingHandle) { clearTimeout(existingHandle); } - const tick = async () => { - const request = await prisma.telegramLoginRequest.findUnique({ - where: { id: requestId }, - select: { status: true }, - }); - - if (!request || request.status !== 'CONFIRMED') { - loginTimerHandles.delete(requestId); - return; - } - - if (expiresAt <= new Date()) { + const handle = setTimeout(() => { + void (async () => { const updated = await prisma.telegramLoginRequest.updateMany({ where: { id: requestId, status: 'CONFIRMED' }, data: { status: 'EXPIRED' }, }); - loginTimerHandles.delete(requestId); + loginExpirationHandles.delete(requestId); if (updated.count > 0) { await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.'); } - return; - } + })(); + }, Math.max(0, expiresAt.getTime() - Date.now())); + loginExpirationHandles.set(requestId, handle); +} - await editLoginMessage(chatId, messageId, loginMessageText(expiresAt), token, expiresAt); - const handle = setTimeout(() => { - void tick(); - }, 1000); - loginTimerHandles.set(requestId, handle); - }; +function cancelLoginExpiration(requestId: string) { + const handle = loginExpirationHandles.get(requestId); + if (handle) { + clearTimeout(handle); + loginExpirationHandles.delete(requestId); + } +} - const handle = setTimeout(() => { - void tick(); - }, 1000); - loginTimerHandles.set(requestId, handle); +async function expireLoginMessageNow( + requestId: string, + chatId: string, + messageId: number, +) { + cancelLoginExpiration(requestId); + const updated = await prisma.telegramLoginRequest.updateMany({ + where: { id: requestId, status: 'CONFIRMED' }, + data: { status: 'EXPIRED' }, + }); + + if (updated.count > 0) { + await editLoginMessage(chatId, messageId, 'Ссылка входа устарела.'); + } +} + +async function refreshExpiredLoginMessage( + requestId: string, + chatId: string, + messageId: number, +) { + const request = await prisma.telegramLoginRequest.findUnique({ + where: { id: requestId }, + select: { status: true }, + }); + + if (request?.status === 'CONFIRMED') { + await expireLoginMessageNow(requestId, chatId, messageId); + } } export async function createTelegramBotLogin() { @@ -257,10 +264,18 @@ export async function completeTelegramBotLogin(token: string) { } if (request.expiresAt <= new Date()) { - await prisma.telegramLoginRequest.update({ - where: { id: request.id }, - data: { status: 'EXPIRED' }, - }); + if (request.telegramChatId && request.telegramMessageId) { + await refreshExpiredLoginMessage( + request.id, + request.telegramChatId, + request.telegramMessageId, + ); + } else { + await prisma.telegramLoginRequest.update({ + where: { id: request.id }, + data: { status: 'EXPIRED' }, + }); + } throw new Error('Telegram login token is expired.'); } @@ -268,6 +283,7 @@ export async function completeTelegramBotLogin(token: string) { throw new Error('Telegram login is not confirmed.'); } + cancelLoginExpiration(request.id); await prisma.telegramLoginRequest.update({ where: { id: request.id }, data: { status: 'USED' }, @@ -314,7 +330,10 @@ export async function handleTelegramBotWebhook( }); if (!request || request.status !== 'PENDING' || request.expiresAt <= new Date()) { - await sendLoginMessage(chatId, 'Ссылка входа устарела.\nВернитесь на сайт и начните вход заново.'); + await sendLoginMessage( + chatId, + 'Ссылка входа устарела.\nВернитесь на сайт и начните вход заново.', + ); return; } @@ -349,9 +368,8 @@ export async function handleTelegramBotWebhook( }, }); - scheduleLoginMessageTimer( + scheduleLoginExpiration( request.id, - token, chatId.toString(), sentMessage.message_id, request.expiresAt,