Add OTP login page and auth guard for client cabinet
This commit is contained in:
178
app/pages/login.vue
Normal file
178
app/pages/login.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user