Files
web-frontend/app/pages/login.vue
2026-04-01 19:10:18 +07:00

179 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable';
import {
RequestLoginCodeDocument,
VerifyLoginCodeDocument,
} from '~/composables/graphql/generated';
type LoginChannel = 'EMAIL' | 'TELEGRAM' | 'MAX';
const config = useRuntimeConfig();
const authCookieName = config.public.authCookieName || 'fregat_auth_token';
const authCookie = useCookie<string | null>(authCookieName, {
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30,
});
const step = ref<'request' | 'verify'>('request');
const channel = ref<LoginChannel>('EMAIL');
const destination = ref('');
const challengeToken = ref('');
const maskedDestination = ref('');
const expiresAt = ref('');
const code = ref('');
const feedback = ref('');
const feedbackTone = ref<'success' | 'error'>('success');
const requestCodeMutation = useMutation(RequestLoginCodeDocument);
const verifyCodeMutation = useMutation(VerifyLoginCodeDocument);
const channelHint = computed(() => {
if (channel.value === 'EMAIL') {
return 'Email адрес';
}
if (channel.value === 'TELEGRAM') {
return 'Telegram channel id';
}
return 'Max channel id';
});
async function requestCode() {
feedback.value = '';
const result = await requestCodeMutation.mutate({
input: {
channel: channel.value,
destination: destination.value.trim(),
},
});
const payload = result?.data?.requestLoginCode;
if (!payload) {
feedback.value = 'Не получилось отправить код.';
feedbackTone.value = 'error';
return;
}
challengeToken.value = payload.challengeToken;
maskedDestination.value = payload.destination;
expiresAt.value = new Date(payload.expiresAt).toLocaleString();
feedback.value = `Код отправлен на ${payload.destination}. Тестовый код сейчас: 123456`;
feedbackTone.value = 'success';
step.value = 'verify';
}
async function verifyCode() {
feedback.value = '';
const result = await verifyCodeMutation.mutate({
input: {
challengeToken: challengeToken.value,
code: code.value.trim(),
},
});
const payload = result?.data?.verifyLoginCode;
if (!payload) {
feedback.value = 'Не получилось выполнить вход.';
feedbackTone.value = 'error';
return;
}
authCookie.value = payload.accessToken;
await navigateTo('/products');
}
</script>
<template>
<section class="mx-auto flex min-h-[calc(100vh-4rem)] w-full max-w-3xl items-center py-8">
<div class="surface-card w-full rounded-[34px] p-5 md:p-8">
<div class="mb-5 text-center">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Вход в личный кабинет</h1>
<p class="mt-1 text-sm text-[#28543f]/80">
Получите одноразовый код и подтвердите вход.
</p>
</div>
<div class="mx-auto mb-5 flex w-full max-w-xl flex-wrap items-center justify-center gap-2">
<button
class="glass-capsule rounded-full px-4 py-2 text-sm font-semibold text-[#123824] transition hover:scale-[1.01]"
:class="{ 'bg-[#139957] text-white': channel === 'EMAIL' }"
@click="channel = 'EMAIL'"
>
Email
</button>
<button
class="glass-capsule rounded-full px-4 py-2 text-sm font-semibold text-[#123824] transition hover:scale-[1.01]"
:class="{ 'bg-[#139957] text-white': channel === 'TELEGRAM' }"
@click="channel = 'TELEGRAM'"
>
Telegram
</button>
<button
class="glass-capsule rounded-full px-4 py-2 text-sm font-semibold text-[#123824] transition hover:scale-[1.01]"
:class="{ 'bg-[#139957] text-white': channel === 'MAX' }"
@click="channel = 'MAX'"
>
Max
</button>
</div>
<div v-if="step === 'request'" class="space-y-3">
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">{{ channelHint }}</span>
<input
v-model="destination"
type="text"
class="input input-bordered border-[#d0e8d8] bg-white/80"
:placeholder="channelHint"
>
</label>
<button
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="requestCodeMutation.loading.value"
@click="requestCode"
>
{{ requestCodeMutation.loading.value ? 'Отправляем…' : 'Получить код' }}
</button>
</div>
<div v-else 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>.
</div>
<label class="form-control">
<span class="label-text font-semibold text-[#194631]">Код подтверждения</span>
<input
v-model="code"
type="text"
maxlength="6"
class="input input-bordered border-[#d0e8d8] bg-white/80"
placeholder="123456"
>
</label>
<button
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="verifyCodeMutation.loading.value"
@click="verifyCode"
>
{{ verifyCodeMutation.loading.value ? 'Проверяем…' : 'Войти' }}
</button>
<button class="btn btn-ghost w-full" @click="step = 'request'">
Выбрать другой канал
</button>
</div>
<div
v-if="feedback"
class="alert mt-4"
:class="feedbackTone === 'success' ? 'alert-success' : 'alert-error'"
>
{{ feedback }}
</div>
</div>
</section>
</template>