Simplify login screen to email-first with bot entry buttons
This commit is contained in:
@@ -6,8 +6,6 @@ import {
|
||||
VerifyLoginCodeDocument,
|
||||
} from '~/composables/graphql/generated';
|
||||
|
||||
type LoginChannel = 'EMAIL' | 'TELEGRAM' | 'MAX';
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const route = useRoute();
|
||||
const authCookieName = config.public.authCookieName || 'fregat_auth_token';
|
||||
@@ -17,15 +15,13 @@ const authCookie = useCookie<string | null>(authCookieName, {
|
||||
});
|
||||
|
||||
const step = ref<'request' | 'verify'>('request');
|
||||
const channel = ref<LoginChannel>('EMAIL');
|
||||
const destination = ref('');
|
||||
const email = ref('');
|
||||
const challengeToken = ref('');
|
||||
const maskedDestination = ref('');
|
||||
const maskedEmail = ref('');
|
||||
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);
|
||||
@@ -33,14 +29,22 @@ const consumeLoginTokenMutation = useMutation(ConsumeLoginTokenDocument);
|
||||
|
||||
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>',
|
||||
);
|
||||
|
||||
const normalizedEmail = computed(() => email.value.trim().toLowerCase());
|
||||
const isEmailReady = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail.value));
|
||||
|
||||
function buildBotLoginUrl(baseUrl: string) {
|
||||
if (!isEmailReady.value || !baseUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const payload = encodeURIComponent(`login:${normalizedEmail.value}`);
|
||||
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||
return `${baseUrl}${separator}start=${payload}`;
|
||||
}
|
||||
|
||||
const telegramLoginUrl = computed(() => buildBotLoginUrl(telegramBotUrl.value));
|
||||
const maxLoginUrl = computed(() => buildBotLoginUrl(maxBotUrl.value));
|
||||
|
||||
async function finalizeSession(accessToken: string) {
|
||||
authCookie.value = accessToken;
|
||||
@@ -48,8 +52,8 @@ async function finalizeSession(accessToken: string) {
|
||||
}
|
||||
|
||||
async function requestCode() {
|
||||
if (channel.value !== 'EMAIL') {
|
||||
feedback.value = 'Кодовый вход доступен только для Email.';
|
||||
if (!isEmailReady.value) {
|
||||
feedback.value = 'Введите корректный email.';
|
||||
feedbackTone.value = 'error';
|
||||
return;
|
||||
}
|
||||
@@ -57,8 +61,8 @@ async function requestCode() {
|
||||
feedback.value = '';
|
||||
const result = await requestCodeMutation.mutate({
|
||||
input: {
|
||||
channel: channel.value,
|
||||
destination: destination.value.trim(),
|
||||
channel: 'EMAIL',
|
||||
destination: normalizedEmail.value,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,9 +74,9 @@ async function requestCode() {
|
||||
}
|
||||
|
||||
challengeToken.value = payload.challengeToken;
|
||||
maskedDestination.value = payload.destination;
|
||||
maskedEmail.value = payload.destination;
|
||||
expiresAt.value = new Date(payload.expiresAt).toLocaleString();
|
||||
feedback.value = `Код отправлен на ${payload.destination}. Тестовый код сейчас: 123456`;
|
||||
feedback.value = `Код отправлен на ${payload.destination}.`;
|
||||
feedbackTone.value = 'success';
|
||||
step.value = 'verify';
|
||||
}
|
||||
@@ -110,19 +114,6 @@ async function consumeLoginToken(loginToken: string) {
|
||||
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) {
|
||||
@@ -138,48 +129,60 @@ onMounted(async () => {
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="text-3xl font-extrabold">Вход в личный кабинет</h1>
|
||||
<p class="mt-1 text-sm text-base-content/70">
|
||||
Email вход по коду. Telegram/Max вход через бота и временный токен.
|
||||
Введите email и выберите способ входа.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 flex justify-center">
|
||||
<div class="join">
|
||||
<button class="btn join-item" :class="{ 'btn-primary': channel === 'EMAIL' }" @click="channel = 'EMAIL'">
|
||||
Email
|
||||
</button>
|
||||
<button class="btn join-item" :class="{ 'btn-primary': channel === 'TELEGRAM' }" @click="channel = 'TELEGRAM'">
|
||||
Telegram
|
||||
</button>
|
||||
<button class="btn join-item" :class="{ 'btn-primary': channel === 'MAX' }" @click="channel = 'MAX'">
|
||||
Max
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="channel === 'EMAIL' && step === 'request'" class="space-y-4">
|
||||
<div v-if="step === 'request'" class="space-y-4">
|
||||
<fieldset class="fieldset rounded-box border border-base-300 bg-base-200/40 p-4">
|
||||
<legend class="fieldset-legend text-sm font-semibold">Email адрес</legend>
|
||||
<label class="label pb-1">На этот адрес отправим код</label>
|
||||
<label class="label pb-1">На этот email придет код и он же пойдет в deep-link бота</label>
|
||||
<input
|
||||
v-model="destination"
|
||||
v-model="email"
|
||||
type="email"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="name@company.com"
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<a
|
||||
:href="telegramLoginUrl || undefined"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-secondary"
|
||||
:class="{ 'btn-disabled pointer-events-none': !telegramLoginUrl }"
|
||||
>
|
||||
Войти через Telegram
|
||||
</a>
|
||||
<a
|
||||
:href="maxLoginUrl || undefined"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-accent"
|
||||
:class="{ 'btn-disabled pointer-events-none': !maxLoginUrl }"
|
||||
>
|
||||
Войти через Max
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="isEmailReady"
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="requestCodeMutation.loading.value"
|
||||
@click="requestCode"
|
||||
>
|
||||
{{ requestCodeMutation.loading.value ? 'Отправляем…' : 'Получить код' }}
|
||||
</button>
|
||||
|
||||
<p v-if="!telegramBotUrl || !maxBotUrl" class="text-xs text-warning">
|
||||
Для кнопок бота нужно задать `NUXT_PUBLIC_TELEGRAM_BOT_URL` и `NUXT_PUBLIC_MAX_BOT_URL`.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="channel === 'EMAIL'" class="space-y-4">
|
||||
<div v-else class="space-y-4">
|
||||
<div class="alert alert-info">
|
||||
Код отправлен на <span class="font-bold">{{ maskedDestination }}</span>.
|
||||
Код отправлен на <span class="font-bold">{{ maskedEmail }}</span>.
|
||||
Действителен до: <span class="font-bold">{{ expiresAt }}</span>.
|
||||
</div>
|
||||
|
||||
@@ -204,48 +207,10 @@ onMounted(async () => {
|
||||
</button>
|
||||
|
||||
<button class="btn btn-ghost w-full" @click="step = 'request'">
|
||||
Выбрать другой канал
|
||||
Вернуться
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div class="alert alert-info">
|
||||
Откройте {{ channel === 'TELEGRAM' ? 'Telegram' : 'Max' }}-бота, отправьте команду
|
||||
<span class="font-bold">{{ startCommand }}</span>, затем передайте боту свой <code>user_id</code>.
|
||||
Бот пришлёт кнопку входа в кабинет с временным токеном.
|
||||
</div>
|
||||
|
||||
<fieldset class="fieldset rounded-box border border-base-300 bg-base-200/40 p-4">
|
||||
<legend class="fieldset-legend text-sm font-semibold">Ваш user_id</legend>
|
||||
<label class="label pb-1">Для команды /start в боте</label>
|
||||
<input
|
||||
v-model="userIdForBot"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="например: cm5abc123xyz"
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<button class="btn btn-outline" @click="copyStartCommand">
|
||||
Скопировать команду
|
||||
</button>
|
||||
<a
|
||||
:href="selectedBotUrl || undefined"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-primary"
|
||||
:class="{ 'btn-disabled pointer-events-none': !selectedBotUrl }"
|
||||
>
|
||||
Открыть бота
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p v-if="!selectedBotUrl" class="text-xs text-error">
|
||||
Ссылка на {{ channel === 'TELEGRAM' ? 'Telegram' : 'Max' }}-бота не настроена в env.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="feedback"
|
||||
class="alert mt-2"
|
||||
|
||||
Reference in New Issue
Block a user