diff --git a/frontend/app.vue b/frontend/app.vue index a08a3a2..010d72a 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -598,6 +598,7 @@ const telegramConnectBusy = ref(false); const telegramRefreshBusy = ref(false); const telegramConnectUrl = ref(""); const telegramConnections = ref([]); +const telegramConnectNotice = ref(""); const telegramStatusLabel = computed(() => { if (telegramConnectStatusLoading.value) return "Checking"; @@ -697,6 +698,39 @@ async function refreshTelegramBusinessConnectionFromApi() { } } +async function completeTelegramBusinessConnectFromToken(token: string) { + const t = String(token || "").trim(); + if (!t) return; + + try { + const result = await $fetch<{ + ok: boolean; + status: string; + businessConnectionId?: string; + }>("/api/omni/telegram/business/connect/complete", { + method: "POST", + body: { token: t }, + }); + + if (result?.ok) { + telegramConnectStatus.value = "connected"; + telegramConnectNotice.value = "Telegram успешно привязан."; + await loadTelegramConnectStatus(); + return; + } + + if (result?.status === "awaiting_telegram_start") { + telegramConnectNotice.value = "Сначала нажмите Start в Telegram, затем нажмите кнопку в боте снова."; + } else if (result?.status === "invalid_or_expired_token") { + telegramConnectNotice.value = "Ссылка привязки истекла. Нажмите Connect в CRM заново."; + } else { + telegramConnectNotice.value = "Не удалось завершить привязку. Запустите Connect заново."; + } + } catch { + telegramConnectNotice.value = "Ошибка завершения привязки. Попробуйте снова."; + } +} + function pilotToUiMessage(message: PilotMessage): UIMessage { return { id: message.id, @@ -2012,6 +2046,14 @@ onMounted(() => { uiPathSyncLocked.value = true; try { + const params = new URLSearchParams(window.location.search); + const tgLinkToken = String(params.get("tg_link_token") ?? "").trim(); + if (tgLinkToken) { + void completeTelegramBusinessConnectFromToken(tgLinkToken); + params.delete("tg_link_token"); + const nextSearch = params.toString(); + window.history.replaceState({}, "", `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ""}`); + } applyPathToUi(window.location.pathname, window.location.search); } finally { uiPathSyncLocked.value = false; @@ -4057,6 +4099,9 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {{ telegramRefreshBusy ? "Syncing..." : "Refresh from API" }} +

+ {{ telegramConnectNotice }} +

diff --git a/frontend/server/api/omni/telegram/business/connect/complete.post.ts b/frontend/server/api/omni/telegram/business/connect/complete.post.ts new file mode 100644 index 0000000..e1e6c8f --- /dev/null +++ b/frontend/server/api/omni/telegram/business/connect/complete.post.ts @@ -0,0 +1,81 @@ +import { readBody } from "h3"; +import { prisma } from "../../../../../utils/prisma"; +import { verifyLinkToken } from "../../../../../utils/telegramBusinessConnect"; + +type CompleteBody = { + token?: string; +}; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const token = String(body?.token ?? "").trim(); + if (!token) { + throw createError({ statusCode: 400, statusMessage: "token is required" }); + } + + const payload = verifyLinkToken(token); + if (!payload) { + return { ok: false, status: "invalid_or_expired_token" }; + } + + const pendingId = `pending:${payload.nonce}`; + const pending = await prisma.telegramBusinessConnection.findFirst({ + where: { + teamId: payload.teamId, + businessConnectionId: pendingId, + }, + }); + + if (!pending) { + return { ok: false, status: "session_not_found" }; + } + + const raw = (pending.rawJson ?? {}) as any; + const telegramUserId = raw?.link?.telegramUserId != null ? String(raw.link.telegramUserId).trim() : ""; + if (!telegramUserId) { + return { ok: false, status: "awaiting_telegram_start" }; + } + + const linkedConnectionId = `link:${telegramUserId}`; + await prisma.$transaction([ + prisma.telegramBusinessConnection.upsert({ + where: { + teamId_businessConnectionId: { + teamId: payload.teamId, + businessConnectionId: linkedConnectionId, + }, + }, + create: { + teamId: payload.teamId, + businessConnectionId: linkedConnectionId, + isEnabled: true, + canReply: true, + rawJson: { + state: "connected", + mode: "token_link", + linkedAt: new Date().toISOString(), + telegramUserId, + tokenNonce: payload.nonce, + }, + }, + update: { + isEnabled: true, + canReply: true, + rawJson: { + state: "connected", + mode: "token_link", + linkedAt: new Date().toISOString(), + telegramUserId, + tokenNonce: payload.nonce, + }, + }, + }), + prisma.telegramBusinessConnection.delete({ where: { id: pending.id } }), + ]); + + return { + ok: true, + status: "connected", + businessConnectionId: linkedConnectionId, + }; +}); diff --git a/frontend/server/api/omni/telegram/business/webhook.post.ts b/frontend/server/api/omni/telegram/business/webhook.post.ts index 7c1196d..1fc3c3f 100644 --- a/frontend/server/api/omni/telegram/business/webhook.post.ts +++ b/frontend/server/api/omni/telegram/business/webhook.post.ts @@ -29,13 +29,25 @@ function crmConnectUrl() { return String(process.env.CRM_APP_URL || "https://clientsflow.dsrptlab.com").trim(); } -function crmConnectButton() { +function crmConnectButton(linkToken?: string) { + const base = crmConnectUrl(); + let target = base; + if (linkToken) { + try { + const u = new URL(base); + u.searchParams.set("tg_link_token", linkToken); + target = u.toString(); + } catch { + target = `${base}${base.includes("?") ? "&" : "?"}tg_link_token=${encodeURIComponent(linkToken)}`; + } + } + return { inline_keyboard: [ [ { text: "Открыть CRM и подтвердить", - url: crmConnectUrl(), + url: target, }, ], ], @@ -105,7 +117,7 @@ export default defineEventHandler(async (event) => { void telegramBotApi("sendMessage", { chat_id: chatId, text: "CRM: связка аккаунта получена. Нажми кнопку ниже и вернись в CRM для подтверждения.", - reply_markup: crmConnectButton(), + reply_markup: crmConnectButton(linkToken), }).catch(() => {}); }