Switch Telegram/Max login to bot temporary token flow

This commit is contained in:
Ruslan Bakiev
2026-04-01 19:20:58 +07:00
parent 1c4fd847dc
commit 9a4dc7bab8
5 changed files with 166 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable';
import {
ConsumeLoginTokenDocument,
RequestLoginCodeDocument,
VerifyLoginCodeDocument,
} from '~/composables/graphql/generated';
@@ -8,6 +9,7 @@ import {
type LoginChannel = 'EMAIL' | 'TELEGRAM' | 'MAX';
const config = useRuntimeConfig();
const route = useRoute();
const authCookieName = config.public.authCookieName || 'fregat_auth_token';
const authCookie = useCookie<string | null>(authCookieName, {
sameSite: 'lax',
@@ -23,21 +25,39 @@ const expiresAt = ref('');
const code = ref('');
const feedback = ref('');
const feedbackTone = ref<'success' | 'error'>('success');
const userIdForBot = ref('');
const requestCodeMutation = useMutation(RequestLoginCodeDocument);
const verifyCodeMutation = useMutation(VerifyLoginCodeDocument);
const consumeLoginTokenMutation = useMutation(ConsumeLoginTokenDocument);
const channelHint = computed(() => {
if (channel.value === 'EMAIL') {
return 'Email адрес';
}
if (channel.value === 'TELEGRAM') {
return 'Telegram channel id';
}
return 'Max channel id';
return 'Email адрес';
});
const telegramBotUrl = computed(() => config.public.telegramBotUrl || '');
const maxBotUrl = computed(() => config.public.maxBotUrl || '');
const selectedBotUrl = computed(() =>
channel.value === 'TELEGRAM'
? telegramBotUrl.value
: maxBotUrl.value,
);
const startCommand = computed(() =>
userIdForBot.value.trim() ? `/start ${userIdForBot.value.trim()}` : '/start <ваш_user_id>',
);
async function finalizeSession(accessToken: string) {
authCookie.value = accessToken;
await navigateTo('/products');
}
async function requestCode() {
if (channel.value !== 'EMAIL') {
feedback.value = 'Кодовый вход доступен только для Email.';
feedbackTone.value = 'error';
return;
}
feedback.value = '';
const result = await requestCodeMutation.mutate({
input: {
@@ -77,9 +97,42 @@ async function verifyCode() {
return;
}
authCookie.value = payload.accessToken;
await navigateTo('/products');
await finalizeSession(payload.accessToken);
}
async function consumeLoginToken(loginToken: string) {
feedback.value = '';
const result = await consumeLoginTokenMutation.mutate({
token: loginToken,
});
const payload = result?.data?.consumeLoginToken;
if (!payload) {
feedback.value = 'Временный токен входа недействителен.';
feedbackTone.value = 'error';
return;
}
await finalizeSession(payload.accessToken);
}
function copyStartCommand() {
navigator.clipboard.writeText(startCommand.value);
feedback.value = `Команда скопирована: ${startCommand.value}`;
feedbackTone.value = 'success';
}
watch(channel, () => {
feedback.value = '';
if (channel.value !== 'EMAIL') {
step.value = 'request';
}
});
onMounted(async () => {
const loginToken = typeof route.query.login_token === 'string' ? route.query.login_token : '';
if (loginToken) {
await consumeLoginToken(loginToken);
}
});
</script>
<template>
@@ -88,7 +141,7 @@ async function verifyCode() {
<div class="mb-5 text-center">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Вход в личный кабинет</h1>
<p class="mt-1 text-sm text-[#28543f]/80">
Получите одноразовый код и подтвердите вход.
Email вход по коду. Telegram/Max вход через бота и временный токен.
</p>
</div>
@@ -116,7 +169,7 @@ async function verifyCode() {
</button>
</div>
<div v-if="step === 'request'" class="space-y-3">
<div v-if="channel === 'EMAIL' && step === 'request'" class="space-y-3">
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">{{ channelHint }}</span>
<input
@@ -136,7 +189,7 @@ async function verifyCode() {
</button>
</div>
<div v-else class="space-y-3">
<div v-else-if="channel === 'EMAIL'" class="space-y-3">
<div class="rounded-xl border border-[#d6ebde] bg-white/75 p-3 text-sm text-[#214735]">
Код отправлен на <span class="font-bold">{{ maskedDestination }}</span>.
Действителен до: <span class="font-bold">{{ expiresAt }}</span>.
@@ -166,6 +219,43 @@ async function verifyCode() {
</button>
</div>
<div v-else class="space-y-4">
<div class="rounded-xl border border-[#d6ebde] bg-white/75 p-3 text-sm text-[#214735]">
Откройте {{ channel === 'TELEGRAM' ? 'Telegram' : 'Max' }}-бота, отправьте команду
<span class="font-bold">{{ startCommand }}</span>, затем передайте боту свой <code>user_id</code>.
Бот пришлёт кнопку входа в кабинет с временным токеном.
</div>
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Ваш user_id (для команды /start)</span>
<input
v-model="userIdForBot"
type="text"
class="input input-bordered border-[#d0e8d8] bg-white/80"
placeholder="например: cm5abc123xyz"
>
</label>
<div class="grid gap-2 sm:grid-cols-2">
<button class="btn btn-outline border-[#139957] text-[#0d854a]" @click="copyStartCommand">
Скопировать команду
</button>
<a
:href="selectedBotUrl || undefined"
target="_blank"
rel="noopener noreferrer"
class="btn border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:class="{ 'btn-disabled pointer-events-none': !selectedBotUrl }"
>
Открыть бота
</a>
</div>
<p v-if="!selectedBotUrl" class="text-xs text-[#b42318]">
Ссылка на {{ channel === 'TELEGRAM' ? 'Telegram' : 'Max' }}-бота не настроена в env.
</p>
</div>
<div
v-if="feedback"
class="alert mt-4"