feat: implement token-complete telegram connect flow via bot button
This commit is contained in:
@@ -598,6 +598,7 @@ const telegramConnectBusy = ref(false);
|
|||||||
const telegramRefreshBusy = ref(false);
|
const telegramRefreshBusy = ref(false);
|
||||||
const telegramConnectUrl = ref("");
|
const telegramConnectUrl = ref("");
|
||||||
const telegramConnections = ref<TelegramConnectionSummary[]>([]);
|
const telegramConnections = ref<TelegramConnectionSummary[]>([]);
|
||||||
|
const telegramConnectNotice = ref("");
|
||||||
|
|
||||||
const telegramStatusLabel = computed(() => {
|
const telegramStatusLabel = computed(() => {
|
||||||
if (telegramConnectStatusLoading.value) return "Checking";
|
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 {
|
function pilotToUiMessage(message: PilotMessage): UIMessage {
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
@@ -2012,6 +2046,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
uiPathSyncLocked.value = true;
|
uiPathSyncLocked.value = true;
|
||||||
try {
|
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);
|
applyPathToUi(window.location.pathname, window.location.search);
|
||||||
} finally {
|
} finally {
|
||||||
uiPathSyncLocked.value = false;
|
uiPathSyncLocked.value = false;
|
||||||
@@ -4057,6 +4099,9 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
{{ telegramRefreshBusy ? "Syncing..." : "Refresh from API" }}
|
{{ telegramRefreshBusy ? "Syncing..." : "Refresh from API" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="telegramConnectNotice" class="text-[11px] leading-snug text-base-content/70">
|
||||||
|
{{ telegramConnectNotice }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 border-t border-base-300 pt-2">
|
<div class="mt-3 border-t border-base-300 pt-2">
|
||||||
|
|||||||
@@ -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<CompleteBody>(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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -29,13 +29,25 @@ function crmConnectUrl() {
|
|||||||
return String(process.env.CRM_APP_URL || "https://clientsflow.dsrptlab.com").trim();
|
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 {
|
return {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: "Открыть CRM и подтвердить",
|
text: "Открыть CRM и подтвердить",
|
||||||
url: crmConnectUrl(),
|
url: target,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -105,7 +117,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
void telegramBotApi("sendMessage", {
|
void telegramBotApi("sendMessage", {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
text: "CRM: связка аккаунта получена. Нажми кнопку ниже и вернись в CRM для подтверждения.",
|
text: "CRM: связка аккаунта получена. Нажми кнопку ниже и вернись в CRM для подтверждения.",
|
||||||
reply_markup: crmConnectButton(),
|
reply_markup: crmConnectButton(linkToken),
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user