1223 lines
46 KiB
Vue
1223 lines
46 KiB
Vue
<template>
|
|
<Transition name="slide-fade">
|
|
<div
|
|
v-if="visible"
|
|
class="fixed inset-0 z-[100] flex items-center justify-center px-4"
|
|
@click.self="$emit('close')"
|
|
>
|
|
<!-- Dark Blurred Backdrop -->
|
|
<div
|
|
class="absolute inset-0 bg-[#062535]/80 backdrop-blur-sm"
|
|
@click="$emit('close')"
|
|
></div>
|
|
|
|
<!-- 3D Perspective Container -->
|
|
<div class="perspective-1000 relative z-10 w-full max-w-md">
|
|
<!-- Flippable Card Body -->
|
|
<div
|
|
class="relative transition-transform duration-700 transform-style-3d"
|
|
:class="{
|
|
'rotate-y-180': authMode === 'register' || authMode === 'resend',
|
|
'rotate-x-180': authMode === 'forgot'
|
|
}"
|
|
>
|
|
<!-- ==================== FRONT FACE: LOGIN ==================== -->
|
|
<form
|
|
@submit.prevent="handleLogin"
|
|
class="backface-hidden w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
|
|
:class="{ 'pointer-events-none': authMode !== 'login' }"
|
|
>
|
|
<!-- Close Button (X) -->
|
|
<button
|
|
type="button"
|
|
@click="$emit('close')"
|
|
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Title -->
|
|
<h2 class="mb-8 text-center text-2xl font-bold text-white">
|
|
{{ t('auth.loginTitle') }}
|
|
</h2>
|
|
|
|
<!-- Email Input -->
|
|
<div class="mb-5">
|
|
<label for="login-email" class="mb-2 block text-sm font-medium text-white/80">
|
|
Email
|
|
</label>
|
|
<input
|
|
id="login-email"
|
|
v-model="email"
|
|
type="email"
|
|
placeholder="pelda@email.com"
|
|
autocomplete="email"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Password Input -->
|
|
<div class="mb-6 relative">
|
|
<label for="login-password" class="mb-2 block text-sm font-medium text-white/80">
|
|
Jelszó
|
|
</label>
|
|
<input
|
|
id="login-password"
|
|
v-model="password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
placeholder="••••••••"
|
|
autocomplete="current-password"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 pr-12 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
<!-- Eye icon (hold to show) -->
|
|
<button
|
|
type="button"
|
|
@mousedown="showPassword = true"
|
|
@mouseup="showPassword = false"
|
|
@mouseleave="showPassword = false"
|
|
@touchstart.prevent="showPassword = true"
|
|
@touchend="showPassword = false"
|
|
class="absolute right-3 top-[42px] text-white/40 hover:text-white/70 transition-colors cursor-pointer"
|
|
tabindex="-1"
|
|
aria-label="Jelszó megtekintése"
|
|
>
|
|
<!-- Eye open (password visible) -->
|
|
<svg
|
|
v-if="showPassword"
|
|
class="h-5 w-5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
|
<path d="M1 1l22 22" />
|
|
</svg>
|
|
<!-- Eye closed (password hidden) -->
|
|
<svg
|
|
v-else
|
|
class="h-5 w-5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Remember Me Checkbox -->
|
|
<div class="mb-4 flex items-center gap-2">
|
|
<input
|
|
id="remember-me"
|
|
v-model="rememberMe"
|
|
type="checkbox"
|
|
class="h-4 w-4 rounded border-white/20 bg-white/5 text-[#14B8A6] focus:ring-2 focus:ring-[#14B8A6]/50 focus:ring-offset-0 cursor-pointer"
|
|
/>
|
|
<label for="remember-me" class="text-sm text-white/70 cursor-pointer select-none hover:text-white/90 transition-colors">
|
|
{{ t('auth.rememberMe') }}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Link Row: Resend (left) + Forgot Password (right) -->
|
|
<div class="flex justify-between items-center w-full mt-2 mb-2 text-sm">
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('resend')"
|
|
class="text-xs text-[#75A882]/70 hover:text-[#D4AF37] transition-colors"
|
|
>
|
|
{{ t('auth.resendLink') }}
|
|
</a>
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('forgot')"
|
|
class="text-xs text-[#75A882] hover:text-[#D4AF37] transition-colors"
|
|
>
|
|
{{ t('auth.forgotPassword') }}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Account Restore Link -->
|
|
<div class="flex justify-center mb-6">
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('restore')"
|
|
class="text-xs text-[#D4AF37]/60 hover:text-[#D4AF37] transition-colors"
|
|
>
|
|
{{ t('auth.restoreLink') }}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Error Message (translated via i18n) -->
|
|
<div
|
|
v-if="authStore.error && authMode === 'login'"
|
|
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
|
|
>
|
|
{{ translateError(authStore.error) }}
|
|
</div>
|
|
|
|
<!-- Login Button (Premium) -->
|
|
<button
|
|
type="submit"
|
|
:disabled="authStore.isLoading"
|
|
class="btn-premium w-full px-4 py-3 text-lg"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : t('auth.loginButton') }}
|
|
</button>
|
|
|
|
<!-- Social Login Divider (Premium) -->
|
|
<div class="my-6 flex items-center gap-3">
|
|
<span class="flex-1 border-t border-white/10"></span>
|
|
<span class="flex items-center gap-2 text-sm text-white/40">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
{{ t('auth.socialDivider') }}
|
|
</span>
|
|
<span class="flex-1 border-t border-white/10"></span>
|
|
</div>
|
|
|
|
<!-- Social Login Buttons -->
|
|
<div class="mb-6 grid grid-cols-2 gap-3">
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/50 transition-all duration-200 hover:border-white/20 hover:text-white/80"
|
|
>
|
|
<!-- Google Logo Placeholder -->
|
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/>
|
|
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
</svg>
|
|
Google
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/50 transition-all duration-200 hover:border-white/20 hover:text-white/80"
|
|
>
|
|
<!-- Apple Logo Placeholder -->
|
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
|
|
</svg>
|
|
Apple
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Registration Link (Triggers Flip) -->
|
|
<p class="text-center text-sm text-white/50">
|
|
{{ t('auth.noAccount') }}
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('register')"
|
|
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
|
|
>
|
|
{{ t('auth.registerLink') }}
|
|
</a>
|
|
</p>
|
|
</form>
|
|
|
|
<!-- ==================== BACK FACE: REGISTER ==================== -->
|
|
<form
|
|
v-if="activeBackFace === 'register'"
|
|
@submit.prevent="handleRegister"
|
|
class="backface-hidden rotate-y-180 absolute inset-0 w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
|
|
>
|
|
<!-- Close Button (X) -->
|
|
<button
|
|
type="button"
|
|
@click="$emit('close')"
|
|
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Title -->
|
|
<h2 class="mb-8 text-center text-2xl font-bold text-white">
|
|
{{ t('auth.registerTitle') }}
|
|
</h2>
|
|
|
|
<!-- Last Name Input -->
|
|
<div class="mb-5">
|
|
<label for="reg-lastname" class="mb-2 block text-sm font-medium text-white/80">
|
|
Vezetéknév
|
|
</label>
|
|
<input
|
|
id="reg-lastname"
|
|
v-model="lastName"
|
|
type="text"
|
|
placeholder="Vezetéknév"
|
|
autocomplete="family-name"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- First Name Input -->
|
|
<div class="mb-5">
|
|
<label for="reg-firstname" class="mb-2 block text-sm font-medium text-white/80">
|
|
Keresztnév
|
|
</label>
|
|
<input
|
|
id="reg-firstname"
|
|
v-model="firstName"
|
|
type="text"
|
|
placeholder="Keresztnév"
|
|
autocomplete="given-name"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Email Input -->
|
|
<div class="mb-5">
|
|
<label for="reg-email" class="mb-2 block text-sm font-medium text-white/80">
|
|
E-mail
|
|
</label>
|
|
<input
|
|
id="reg-email"
|
|
v-model="regEmail"
|
|
type="email"
|
|
placeholder="pelda@email.com"
|
|
autocomplete="email"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Password Input -->
|
|
<div class="mb-6 relative">
|
|
<label for="reg-password" class="mb-2 block text-sm font-medium text-white/80">
|
|
Jelszó
|
|
</label>
|
|
<input
|
|
id="reg-password"
|
|
v-model="regPassword"
|
|
:type="showRegPassword ? 'text' : 'password'"
|
|
placeholder="••••••••"
|
|
autocomplete="new-password"
|
|
required
|
|
minlength="8"
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 pr-12 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
<!-- Eye icon (hold to show) -->
|
|
<button
|
|
type="button"
|
|
@mousedown="showRegPassword = true"
|
|
@mouseup="showRegPassword = false"
|
|
@mouseleave="showRegPassword = false"
|
|
@touchstart.prevent="showRegPassword = true"
|
|
@touchend="showRegPassword = false"
|
|
class="absolute right-3 top-[42px] text-white/40 hover:text-white/70 transition-colors cursor-pointer"
|
|
tabindex="-1"
|
|
aria-label="Jelszó megtekintése"
|
|
>
|
|
<!-- Eye open (password visible) -->
|
|
<svg
|
|
v-if="showRegPassword"
|
|
class="h-5 w-5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
|
<path d="M1 1l22 22" />
|
|
</svg>
|
|
<!-- Eye closed (password hidden) -->
|
|
<svg
|
|
v-else
|
|
class="h-5 w-5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error Message (translated via i18n) -->
|
|
<div
|
|
v-if="authStore.error && authMode === 'register'"
|
|
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
|
|
>
|
|
{{ translateError(authStore.error) }}
|
|
</div>
|
|
|
|
<!-- Register Button (Premium) -->
|
|
<button
|
|
type="submit"
|
|
:disabled="authStore.isLoading"
|
|
class="btn-premium w-full px-4 py-3 text-lg"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : t('auth.registerButton') }}
|
|
</button>
|
|
|
|
<!-- Switch back to Login -->
|
|
<p class="mt-6 text-center text-sm text-white/50">
|
|
{{ t('auth.haveAccount') }}
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('login')"
|
|
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
|
|
>
|
|
{{ t('auth.loginLink') }}
|
|
</a>
|
|
</p>
|
|
</form>
|
|
|
|
<!-- ==================== THIRD FACE: FORGOT PASSWORD ==================== -->
|
|
<form
|
|
v-if="activeBackFace === 'forgot'"
|
|
@submit.prevent="handleForgotPassword"
|
|
class="backface-hidden rotate-x-180 absolute inset-0 w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
|
|
>
|
|
<!-- Close Button (X) -->
|
|
<button
|
|
type="button"
|
|
@click="$emit('close')"
|
|
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Title -->
|
|
<h2 class="mb-6 text-center text-2xl font-bold text-white">
|
|
{{ t('auth.forgotTitle') }}
|
|
</h2>
|
|
|
|
<!-- Description (only show before successful send) -->
|
|
<p
|
|
v-if="!forgotSuccess && !isUserInactive"
|
|
class="mb-6 rounded-xl bg-[#1A6B5A]/20 px-4 py-3 text-center text-sm text-[#5EC4B0]"
|
|
>
|
|
{{ t('auth.forgotDescription') }}
|
|
</p>
|
|
|
|
<!-- Success Message -->
|
|
<div
|
|
v-if="forgotSuccess"
|
|
class="mb-6 rounded-xl bg-[#70BC84]/20 px-4 py-3 text-center text-sm text-[#70BC84]"
|
|
>
|
|
{{ t('auth.forgotSuccess') }}
|
|
</div>
|
|
|
|
<!-- Inactive User Message + Resend Activation Button -->
|
|
<div
|
|
v-if="isUserInactive"
|
|
class="mb-6 rounded-xl bg-amber-500/20 px-4 py-4 text-center"
|
|
>
|
|
<p class="mb-3 text-sm text-amber-300">
|
|
{{ t('auth.inactiveMessage') }}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
:disabled="authStore.isLoading"
|
|
@click="handleResendActivation"
|
|
class="rounded-lg bg-[#D4AF37] px-5 py-2 text-sm font-semibold text-[#062535] transition-all hover:bg-[#D4AF37]/90 disabled:opacity-50"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : t('auth.resendActivationButton') }}
|
|
</button>
|
|
<p v-if="resendSuccess" class="mt-2 text-xs text-[#70BC84]">
|
|
{{ t('auth.resendActivationSuccess') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Email Input (hide after successful send) -->
|
|
<div v-if="!forgotSuccess && !isUserInactive" class="mb-6">
|
|
<label for="forgot-email" class="mb-2 block text-sm font-medium text-white/80">
|
|
E-mail
|
|
</label>
|
|
<input
|
|
id="forgot-email"
|
|
v-model="forgotEmail"
|
|
type="email"
|
|
placeholder="pelda@email.com"
|
|
autocomplete="email"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Error Message (non-inactive errors, translated) -->
|
|
<div
|
|
v-if="forgotError && !isUserInactive"
|
|
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
|
|
>
|
|
{{ translateError(forgotError) }}
|
|
</div>
|
|
|
|
<!-- Send Link Button (Premium) — hide after successful send or inactive -->
|
|
<button
|
|
v-if="!forgotSuccess && !isUserInactive"
|
|
type="submit"
|
|
:disabled="authStore.isLoading"
|
|
class="btn-premium w-full px-4 py-3 text-lg"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : t('auth.forgotSendButton') }}
|
|
</button>
|
|
|
|
<!-- Back to Login (always visible) -->
|
|
<p class="mt-6 text-center text-sm text-white/50">
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('login')"
|
|
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
|
|
>
|
|
{{ t('auth.backToLogin') }}
|
|
</a>
|
|
</p>
|
|
</form>
|
|
|
|
<!-- ==================== FOURTH FACE: RESEND ACTIVATION ==================== -->
|
|
<form
|
|
v-if="activeBackFace === 'resend'"
|
|
@submit.prevent="handleResend"
|
|
class="backface-hidden rotate-y-180 absolute inset-0 w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
|
|
>
|
|
<!-- Close Button (X) -->
|
|
<button
|
|
type="button"
|
|
@click="$emit('close')"
|
|
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Title -->
|
|
<h2 class="mb-6 text-center text-2xl font-bold text-white">
|
|
{{ t('auth.resendTitle') }}
|
|
</h2>
|
|
|
|
<!-- Description (only show before successful send) -->
|
|
<p
|
|
v-if="!resendSuccess"
|
|
class="mb-6 rounded-xl bg-[#1A6B5A]/20 px-4 py-3 text-center text-sm text-[#5EC4B0]"
|
|
>
|
|
{{ t('auth.resendDescription') }}
|
|
</p>
|
|
|
|
<!-- Success Card (green checkmark) -->
|
|
<div
|
|
v-if="resendSuccess"
|
|
class="mb-6 rounded-xl bg-[#70BC84]/20 px-4 py-6 text-center"
|
|
>
|
|
<div class="mb-3 flex justify-center">
|
|
<svg class="h-12 w-12 text-[#70BC84]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-[#70BC84]">
|
|
{{ t('auth.resendSuccess') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Email Input (hide after successful send) -->
|
|
<div v-if="!resendSuccess" class="mb-6">
|
|
<label for="resend-email" class="mb-2 block text-sm font-medium text-white/80">
|
|
E-mail
|
|
</label>
|
|
<input
|
|
id="resend-email"
|
|
v-model="email"
|
|
type="email"
|
|
placeholder="pelda@email.com"
|
|
autocomplete="email"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Error Message (translated) -->
|
|
<div
|
|
v-if="resendError && !resendSuccess"
|
|
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
|
|
>
|
|
{{ translateError(resendError) }}
|
|
</div>
|
|
|
|
<!-- Send Button (Premium) — hide after success, disabled during cooldown -->
|
|
<button
|
|
v-if="!resendSuccess"
|
|
type="submit"
|
|
:disabled="resendCooldown > 0 || authStore.isLoading"
|
|
class="btn-premium w-full px-4 py-3 text-lg"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : (resendCooldown > 0 ? t('auth.resend_cooldown', { seconds: resendCooldown }) : t('auth.send_email')) }}
|
|
</button>
|
|
|
|
<!-- Back to Login (always visible) -->
|
|
<p class="mt-6 text-center text-sm text-white/50">
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('login')"
|
|
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
|
|
>
|
|
{{ t('auth.backToLogin') }}
|
|
</a>
|
|
</p>
|
|
</form>
|
|
|
|
<!-- ==================== FIFTH FACE: ACCOUNT RESTORE ==================== -->
|
|
<form
|
|
v-if="activeBackFace === 'restore'"
|
|
@submit.prevent="handleRestoreStep"
|
|
class="backface-hidden rotate-y-180 absolute inset-0 w-full rounded-2xl border border-[#75A882]/30 bg-[#062535] p-8 shadow-2xl"
|
|
>
|
|
<!-- Close Button (X) -->
|
|
<button
|
|
type="button"
|
|
@click="$emit('close')"
|
|
class="absolute right-4 top-4 text-white/60 transition-colors hover:text-white"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Title -->
|
|
<h2 class="mb-6 text-center text-2xl font-bold text-white">
|
|
{{ t('auth.restoreTitle') }}
|
|
</h2>
|
|
|
|
<!-- Step 1: Email + Send Code -->
|
|
<template v-if="restoreStep === 1">
|
|
<p class="mb-6 rounded-xl bg-[#1A6B5A]/20 px-4 py-3 text-center text-sm text-[#5EC4B0]">
|
|
{{ t('auth.restoreStep1') }}
|
|
</p>
|
|
|
|
<!-- Email Input -->
|
|
<div class="mb-6">
|
|
<label for="restore-email" class="mb-2 block text-sm font-medium text-white/80">
|
|
{{ t('auth.restoreEmailLabel') }}
|
|
</label>
|
|
<input
|
|
id="restore-email"
|
|
v-model="restoreEmail"
|
|
type="email"
|
|
placeholder="pelda@email.com"
|
|
autocomplete="email"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<div
|
|
v-if="restoreError"
|
|
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
|
|
>
|
|
{{ translateError(restoreError) }}
|
|
</div>
|
|
|
|
<!-- Send Code Button -->
|
|
<button
|
|
type="submit"
|
|
:disabled="authStore.isLoading"
|
|
class="btn-premium w-full px-4 py-3 text-lg"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : t('auth.restoreSendCode') }}
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Step 2: OTP + New Password -->
|
|
<template v-else-if="restoreStep === 2">
|
|
<p class="mb-6 rounded-xl bg-[#1A6B5A]/20 px-4 py-3 text-center text-sm text-[#5EC4B0]">
|
|
{{ t('auth.restoreStep2') }}
|
|
</p>
|
|
|
|
<!-- Code Sent Info -->
|
|
<div class="mb-4 rounded-xl bg-[#1A6B5A]/10 px-4 py-2 text-center text-xs text-[#5EC4B0]">
|
|
{{ t('auth.restoreCodeSent') }}
|
|
</div>
|
|
|
|
<!-- OTP Input -->
|
|
<div class="mb-5">
|
|
<label for="restore-otp" class="mb-2 block text-sm font-medium text-white/80">
|
|
{{ t('auth.restoreOtpLabel') }}
|
|
</label>
|
|
<input
|
|
id="restore-otp"
|
|
v-model="restoreOtp"
|
|
type="text"
|
|
inputmode="numeric"
|
|
pattern="[0-9]{6}"
|
|
maxlength="6"
|
|
placeholder="123456"
|
|
autocomplete="one-time-code"
|
|
required
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- New Password Input -->
|
|
<div class="mb-6 relative">
|
|
<label for="restore-password" class="mb-2 block text-sm font-medium text-white/80">
|
|
{{ t('auth.restoreNewPassword') }}
|
|
</label>
|
|
<input
|
|
id="restore-password"
|
|
v-model="restoreNewPassword"
|
|
:type="showRestorePassword ? 'text' : 'password'"
|
|
placeholder="••••••••"
|
|
autocomplete="new-password"
|
|
required
|
|
minlength="6"
|
|
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 pr-12 text-white placeholder-white/30 backdrop-blur-sm transition-all duration-200 focus:border-[#75A882] focus:outline-none focus:ring-2 focus:ring-[#75A882]/50"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@mousedown="showRestorePassword = true"
|
|
@mouseup="showRestorePassword = false"
|
|
@mouseleave="showRestorePassword = false"
|
|
@touchstart.prevent="showRestorePassword = true"
|
|
@touchend="showRestorePassword = false"
|
|
class="absolute right-3 top-[42px] text-white/40 hover:text-white/70 transition-colors cursor-pointer"
|
|
tabindex="-1"
|
|
aria-label="Jelszó megtekintése"
|
|
>
|
|
<svg
|
|
v-if="showRestorePassword"
|
|
class="h-5 w-5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
|
<path d="M1 1l22 22" />
|
|
</svg>
|
|
<svg
|
|
v-else
|
|
class="h-5 w-5"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<div
|
|
v-if="restoreError"
|
|
class="mb-4 rounded-xl bg-red-500/20 px-4 py-3 text-sm text-red-300"
|
|
>
|
|
{{ translateError(restoreError) }}
|
|
</div>
|
|
|
|
<!-- Verify Button -->
|
|
<button
|
|
type="submit"
|
|
:disabled="authStore.isLoading"
|
|
class="btn-premium w-full px-4 py-3 text-lg"
|
|
>
|
|
{{ authStore.isLoading ? t('auth.loginLoading') : t('auth.restoreVerifyButton') }}
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Success Message -->
|
|
<template v-else-if="restoreStep === 3">
|
|
<div class="mb-6 rounded-xl bg-[#70BC84]/20 px-4 py-6 text-center">
|
|
<div class="mb-3 flex justify-center">
|
|
<svg class="h-12 w-12 text-[#70BC84]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-[#70BC84]">
|
|
{{ t('auth.restoreSuccess') }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Back to Login -->
|
|
<p class="mt-6 text-center text-sm text-white/50">
|
|
<a
|
|
href="#"
|
|
@click.prevent="switchMode('login')"
|
|
class="font-semibold text-[#D4AF37] transition-colors hover:text-[#D4AF37]/80"
|
|
>
|
|
{{ t('auth.backToLogin') }}
|
|
</a>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import FingerprintJS from '@fingerprintjs/fingerprintjs'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
close: []
|
|
}>()
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
|
|
// ── Device Fingerprinting (FingerprintJS) ──
|
|
let fpPromise: ReturnType<typeof FingerprintJS.load> | null = null
|
|
|
|
/**
|
|
* Generate a device fingerprint hash asynchronously.
|
|
* Returns the visitorId string, or null if FingerprintJS fails to load.
|
|
*/
|
|
async function getDeviceFingerprint(): Promise<string | null> {
|
|
try {
|
|
if (!fpPromise) {
|
|
fpPromise = FingerprintJS.load()
|
|
}
|
|
const fp = await fpPromise
|
|
const result = await fp.get()
|
|
return result.visitorId
|
|
} catch (err) {
|
|
console.warn('FingerprintJS failed to load:', err)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// ── Login form fields (persisted across modal close) ──
|
|
const email = ref(localStorage.getItem('saved-email') || '')
|
|
const password = ref('')
|
|
|
|
// ── Register form fields (persisted across modal close) ──
|
|
const lastName = ref('')
|
|
const firstName = ref('')
|
|
const regEmail = ref('')
|
|
const regPassword = ref('')
|
|
|
|
// ── Forgot password form fields ──
|
|
const forgotEmail = ref('')
|
|
const forgotError = ref('')
|
|
const forgotSuccess = ref(false)
|
|
const isUserInactive = ref(false)
|
|
const resendSuccess = ref(false)
|
|
|
|
// ── Resend form fields ──
|
|
const resendError = ref('')
|
|
|
|
// ── Resend cooldown (60s spam protection, persisted in localStorage) ──
|
|
const resendCooldown = ref(0)
|
|
let cooldownTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
function startCooldown() {
|
|
const expiry = Date.now() + 60000
|
|
localStorage.setItem('resend_cooldown_until', expiry.toString())
|
|
resendCooldown.value = 60
|
|
if (cooldownTimer) clearInterval(cooldownTimer)
|
|
cooldownTimer = setInterval(() => {
|
|
const remaining = Math.max(0, Math.round((expiry - Date.now()) / 1000))
|
|
resendCooldown.value = remaining
|
|
if (remaining <= 0) {
|
|
if (cooldownTimer) {
|
|
clearInterval(cooldownTimer)
|
|
cooldownTimer = null
|
|
}
|
|
localStorage.removeItem('resend_cooldown_until')
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
// ── Password visibility states (hold-to-show) ──
|
|
const showPassword = ref(false)
|
|
const showRegPassword = ref(false)
|
|
|
|
// ── Remember Me state (persisted in localStorage) ──
|
|
const rememberMe = ref(localStorage.getItem('remember-me-state') === 'true')
|
|
|
|
// ── 3D Flip state machine ──
|
|
const authMode = ref<'login' | 'register' | 'forgot' | 'resend' | 'restore'>('login')
|
|
const activeBackFace = ref<'register' | 'forgot' | 'resend' | 'restore'>('register')
|
|
|
|
// ── Account Restore form fields ──
|
|
const restoreEmail = ref('')
|
|
const restoreOtp = ref('')
|
|
const restoreNewPassword = ref('')
|
|
const restoreError = ref('')
|
|
const restoreStep = ref(1) // 1 = email, 2 = otp+password, 3 = success
|
|
const showRestorePassword = ref(false)
|
|
|
|
// ── Restore saved email, remember-me state, and resend cooldown on mount ──
|
|
onMounted(() => {
|
|
const saved = localStorage.getItem('saved-email')
|
|
if (saved) {
|
|
email.value = saved
|
|
}
|
|
// Restore remember-me checkbox state
|
|
rememberMe.value = localStorage.getItem('remember-me-state') === 'true'
|
|
|
|
// Restore resend cooldown from localStorage (survives page refresh)
|
|
const storedExpiry = localStorage.getItem('resend_cooldown_until')
|
|
if (storedExpiry) {
|
|
const remaining = Math.max(0, Math.round((parseInt(storedExpiry, 10) - Date.now()) / 1000))
|
|
if (remaining > 0) {
|
|
resendCooldown.value = remaining
|
|
// Resume the countdown
|
|
if (cooldownTimer) clearInterval(cooldownTimer)
|
|
cooldownTimer = setInterval(() => {
|
|
const rem = Math.max(0, Math.round((parseInt(storedExpiry, 10) - Date.now()) / 1000))
|
|
resendCooldown.value = rem
|
|
if (rem <= 0) {
|
|
if (cooldownTimer) {
|
|
clearInterval(cooldownTimer)
|
|
cooldownTimer = null
|
|
}
|
|
localStorage.removeItem('resend_cooldown_until')
|
|
}
|
|
}, 1000)
|
|
} else {
|
|
localStorage.removeItem('resend_cooldown_until')
|
|
}
|
|
}
|
|
})
|
|
|
|
// ── Clean up interval on unmount ──
|
|
onUnmounted(() => {
|
|
if (cooldownTimer) {
|
|
clearInterval(cooldownTimer)
|
|
cooldownTimer = null
|
|
}
|
|
})
|
|
|
|
// ── On modal close: keep form fields, just reset view and clear errors ──
|
|
watch(
|
|
() => props.visible,
|
|
(newVal) => {
|
|
if (!newVal) {
|
|
// Reset view to login
|
|
authMode.value = 'login'
|
|
activeBackFace.value = 'register'
|
|
// Reset restore state
|
|
restoreStep.value = 1
|
|
restoreEmail.value = ''
|
|
restoreOtp.value = ''
|
|
restoreNewPassword.value = ''
|
|
restoreError.value = ''
|
|
forgotError.value = ''
|
|
forgotSuccess.value = false
|
|
isUserInactive.value = false
|
|
resendSuccess.value = false
|
|
resendError.value = ''
|
|
// Clear store error
|
|
authStore.clearError()
|
|
}
|
|
}
|
|
)
|
|
|
|
// ── Clear store error when switching modes ──
|
|
watch(authMode, () => {
|
|
authStore.clearError()
|
|
})
|
|
|
|
function switchMode(mode: 'login' | 'register' | 'forgot' | 'resend' | 'restore') {
|
|
if (mode !== 'login') {
|
|
activeBackFace.value = mode
|
|
}
|
|
authMode.value = mode
|
|
// Reset restore state when switching away from restore
|
|
if (mode !== 'restore') {
|
|
restoreStep.value = 1
|
|
restoreEmail.value = ''
|
|
restoreOtp.value = ''
|
|
restoreNewPassword.value = ''
|
|
restoreError.value = ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Translate an error value that may be an i18n key (e.g. 'auth.invalid_credentials')
|
|
* or a raw string. If it starts with 'auth.', treat it as an i18n key and translate it.
|
|
* Otherwise, return the raw string as-is.
|
|
*/
|
|
function translateError(msg: string): string {
|
|
if (!msg) return ''
|
|
if (msg.startsWith('auth.')) {
|
|
const translated = t(msg)
|
|
// If translation returns the key itself (missing), fall back to raw
|
|
if (translated !== msg) return translated
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// ── Smart Login Logic (with Device Fingerprinting) ──
|
|
async function handleLogin() {
|
|
try {
|
|
// Generate device fingerprint asynchronously (non-blocking)
|
|
const deviceFingerprint = await getDeviceFingerprint()
|
|
|
|
await authStore.login(email.value, password.value, deviceFingerprint || undefined)
|
|
|
|
// Persist remember-me state and email
|
|
if (rememberMe.value) {
|
|
localStorage.setItem('remember-me-state', 'true')
|
|
localStorage.setItem('saved-email', email.value)
|
|
} else {
|
|
localStorage.setItem('remember-me-state', 'false')
|
|
localStorage.removeItem('saved-email')
|
|
}
|
|
|
|
// After successful login, check KYC status
|
|
if (authStore.isKycComplete) {
|
|
router.push('/dashboard')
|
|
} else {
|
|
router.push('/complete-kyc')
|
|
}
|
|
|
|
// Close the modal
|
|
emit('close')
|
|
} catch {
|
|
// Error is already set in authStore.error — UI displays it automatically via translateError
|
|
}
|
|
}
|
|
|
|
// ── Register Logic ──
|
|
async function handleRegister() {
|
|
try {
|
|
await authStore.register({
|
|
email: regEmail.value,
|
|
password: regPassword.value,
|
|
first_name: firstName.value,
|
|
last_name: lastName.value,
|
|
})
|
|
|
|
// Save email to localStorage for next visit
|
|
localStorage.setItem('saved-email', regEmail.value)
|
|
|
|
// Registration successful — redirect to email verification page
|
|
emit('close')
|
|
router.push('/verify')
|
|
} catch {
|
|
// Error is already set in authStore.error
|
|
}
|
|
}
|
|
|
|
// ── Forgot Password Logic ──
|
|
async function handleForgotPassword() {
|
|
forgotError.value = ''
|
|
isUserInactive.value = false
|
|
resendSuccess.value = false
|
|
try {
|
|
await authStore.forgotPassword(forgotEmail.value)
|
|
|
|
// Success — show green success message and "Back to login" button
|
|
forgotSuccess.value = true
|
|
forgotEmail.value = ''
|
|
} catch {
|
|
const errMsg = authStore.error || ''
|
|
// Check if the error is about inactive user
|
|
if (errMsg === 'auth.user_inactive') {
|
|
isUserInactive.value = true
|
|
forgotError.value = errMsg
|
|
} else {
|
|
// Show error message from the store (already an i18n key or raw string)
|
|
forgotError.value = errMsg || 'auth.forgotFailed'
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Resend Activation Email Logic (from forgot view) ──
|
|
async function handleResendActivation() {
|
|
resendSuccess.value = false
|
|
try {
|
|
await authStore.resendVerification(forgotEmail.value)
|
|
resendSuccess.value = true
|
|
} catch {
|
|
// Error is already set in authStore.error
|
|
}
|
|
}
|
|
|
|
// ── Resend Verification Email (from resend view) ──
|
|
async function handleResend() {
|
|
resendError.value = ''
|
|
resendSuccess.value = false
|
|
try {
|
|
await authStore.resendVerification(email.value)
|
|
// Success — show green success card
|
|
resendSuccess.value = true
|
|
// Start 60s cooldown ONLY after successful send
|
|
startCooldown()
|
|
} catch {
|
|
const errMsg = authStore.error || ''
|
|
resendError.value = errMsg || 'auth.resendFailed'
|
|
}
|
|
}
|
|
|
|
// ── Account Restore Logic ──
|
|
async function handleRestoreStep() {
|
|
restoreError.value = ''
|
|
|
|
if (restoreStep.value === 1) {
|
|
// Step 1: Send restoration code to email
|
|
try {
|
|
await authStore.requestRestore(restoreEmail.value)
|
|
// Move to step 2
|
|
restoreStep.value = 2
|
|
} catch (err: any) {
|
|
// Capture error from store — display user-friendly message
|
|
restoreError.value = authStore.error || err?.response?.data?.detail || 'auth.restoreError'
|
|
// Log the actual error for debugging
|
|
console.warn('Account restore step 1 failed:', err)
|
|
} finally {
|
|
// GUARANTEE: loading state is always reset, even if catch throws
|
|
authStore.isLoading = false
|
|
}
|
|
} else if (restoreStep.value === 2) {
|
|
// Step 2: Verify OTP + set new password
|
|
try {
|
|
await authStore.verifyRestore(restoreEmail.value, restoreOtp.value, restoreNewPassword.value)
|
|
// Success — show success message
|
|
restoreStep.value = 3
|
|
// Auto-close modal and redirect after short delay
|
|
setTimeout(() => {
|
|
emit('close')
|
|
if (authStore.isKycComplete) {
|
|
router.push('/dashboard')
|
|
} else {
|
|
router.push('/complete-kyc')
|
|
}
|
|
}, 2000)
|
|
} catch (err: any) {
|
|
// Capture error from store — display user-friendly message
|
|
restoreError.value = authStore.error || err?.response?.data?.detail || 'auth.restoreError'
|
|
// Log the actual error for debugging
|
|
console.warn('Account restore step 2 failed:', err)
|
|
} finally {
|
|
// GUARANTEE: loading state is always reset, even if catch throws
|
|
authStore.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Slide-Fade Transition: enters from top-right, fades in — slower & more elegant */
|
|
.slide-fade-enter-active {
|
|
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
|
}
|
|
.slide-fade-leave-active {
|
|
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
|
}
|
|
.slide-fade-enter-from {
|
|
opacity: 0;
|
|
transform: translateX(48px) translateY(-48px) scale(0.95);
|
|
}
|
|
.slide-fade-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(48px) translateY(-48px) scale(0.95);
|
|
}
|
|
|
|
/* 3D Card Flip Utilities */
|
|
.perspective-1000 {
|
|
perspective: 1000px;
|
|
}
|
|
.transform-style-3d {
|
|
transform-style: preserve-3d;
|
|
}
|
|
.backface-hidden {
|
|
backface-visibility: hidden;
|
|
}
|
|
.rotate-y-180 {
|
|
transform: rotateY(180deg);
|
|
}
|
|
.rotate-x-180 {
|
|
transform: rotateX(180deg);
|
|
}
|
|
</style> |