Use password login without OTP; keep OTP for signup and optional mobile login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -45,16 +45,23 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
clearLegacy();
|
||||
}
|
||||
|
||||
async function startLogin(identifier: string, password: string) {
|
||||
const { data } = await api.post<LoginChallengeResponse>('/auth/login', {
|
||||
async function login(identifier: string, password: string) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/login', {
|
||||
identifier,
|
||||
password,
|
||||
});
|
||||
persist(data);
|
||||
}
|
||||
|
||||
async function requestLoginOtp(mobile: string) {
|
||||
const { data } = await api.post<LoginChallengeResponse>('/auth/login/otp', {
|
||||
mobile,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async function verifyLogin(challengeId: string, code: string) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/login/verify', {
|
||||
async function verifyLoginOtp(challengeId: string, code: string) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/login/otp/verify', {
|
||||
challengeId,
|
||||
code,
|
||||
});
|
||||
@@ -71,13 +78,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
async function verifyRegister(
|
||||
challengeId: string,
|
||||
emailCode: string,
|
||||
mobileCode: string,
|
||||
emailCode?: string,
|
||||
) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/register/verify', {
|
||||
challengeId,
|
||||
emailCode,
|
||||
mobileCode,
|
||||
...(emailCode ? { emailCode } : {}),
|
||||
});
|
||||
persist(data);
|
||||
}
|
||||
@@ -94,8 +101,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
token,
|
||||
user,
|
||||
isAuthenticated,
|
||||
startLogin,
|
||||
verifyLogin,
|
||||
login,
|
||||
requestLoginOtp,
|
||||
verifyLoginOtp,
|
||||
startRegister,
|
||||
verifyRegister,
|
||||
logout,
|
||||
|
||||
@@ -11,9 +11,13 @@ const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const step = ref<'credentials' | 'otp'>('credentials');
|
||||
/** password = ورود با رمز | otp = ورود با کد موبایل */
|
||||
const mode = ref<'password' | 'otp'>('password');
|
||||
const otpStep = ref<'mobile' | 'code'>('mobile');
|
||||
|
||||
const identifier = ref('owner@demo.local');
|
||||
const password = ref('demo1234');
|
||||
const mobile = ref('');
|
||||
const showPassword = ref(false);
|
||||
const remember = ref(true);
|
||||
const otpCode = ref('');
|
||||
@@ -22,11 +26,8 @@ const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
const otpHint = computed(() => {
|
||||
if (!challenge.value) return '';
|
||||
const dest = challenge.value.maskedDestination ?? '';
|
||||
return challenge.value.channel === 'email'
|
||||
? `کد ارسالشده به ایمیل ${dest} را وارد کنید`
|
||||
: `کد ارسالشده به موبایل ${dest} را وارد کنید`;
|
||||
const dest = challenge.value?.maskedDestination ?? '';
|
||||
return dest ? `کد ارسالشده به ${dest} را وارد کنید` : 'کد یکبارمصرف را وارد کنید';
|
||||
});
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
@@ -40,15 +41,17 @@ function apiError(e: unknown, fallback: string) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function submitCredentials() {
|
||||
async function goHome() {
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
|
||||
await router.push(redirect);
|
||||
}
|
||||
|
||||
async function submitPasswordLogin() {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
challenge.value = await auth.startLogin(identifier.value.trim(), password.value);
|
||||
if (challenge.value.devCode) {
|
||||
otpCode.value = challenge.value.devCode;
|
||||
}
|
||||
step.value = 'otp';
|
||||
await auth.login(identifier.value.trim(), password.value);
|
||||
await goHome();
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ورود ناموفق بود');
|
||||
} finally {
|
||||
@@ -56,14 +59,27 @@ async function submitCredentials() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOtp() {
|
||||
async function submitOtpMobile() {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
challenge.value = await auth.requestLoginOtp(mobile.value.trim());
|
||||
if (challenge.value.devCode) otpCode.value = challenge.value.devCode;
|
||||
otpStep.value = 'code';
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ارسال کد ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOtpCode() {
|
||||
if (!challenge.value) return;
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.verifyLogin(challenge.value.challengeId, otpCode.value.trim());
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
|
||||
await router.push(redirect);
|
||||
await auth.verifyLoginOtp(challenge.value.challengeId, otpCode.value.trim());
|
||||
await goHome();
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
||||
} finally {
|
||||
@@ -71,8 +87,16 @@ async function submitOtp() {
|
||||
}
|
||||
}
|
||||
|
||||
function backToCredentials() {
|
||||
step.value = 'credentials';
|
||||
function switchMode(next: 'password' | 'otp') {
|
||||
mode.value = next;
|
||||
otpStep.value = 'mobile';
|
||||
challenge.value = null;
|
||||
otpCode.value = '';
|
||||
error.value = '';
|
||||
}
|
||||
|
||||
function backToOtpMobile() {
|
||||
otpStep.value = 'mobile';
|
||||
otpCode.value = '';
|
||||
challenge.value = null;
|
||||
error.value = '';
|
||||
@@ -92,11 +116,32 @@ function backToCredentials() {
|
||||
</header>
|
||||
|
||||
<div class="form-inner">
|
||||
<template v-if="step === 'credentials'">
|
||||
<h1>خوش آمدید 👋</h1>
|
||||
<p class="lead">برای ورود به حساب کاربری خود، اطلاعاتتان را وارد کنید</p>
|
||||
<h1>خوش آمدید 👋</h1>
|
||||
<p class="lead">برای ورود به حساب کاربری خود، اطلاعاتتان را وارد کنید</p>
|
||||
|
||||
<form class="form" @submit.prevent="submitCredentials">
|
||||
<div class="mode-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mode === 'password'"
|
||||
:class="{ on: mode === 'password' }"
|
||||
@click="switchMode('password')"
|
||||
>
|
||||
ورود با رمز
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="mode === 'otp'"
|
||||
:class="{ on: mode === 'otp' }"
|
||||
@click="switchMode('otp')"
|
||||
>
|
||||
ورود با کد یکبارمصرف
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="mode === 'password'">
|
||||
<form class="form" @submit.prevent="submitPasswordLogin">
|
||||
<div class="field">
|
||||
<label for="identifier">ایمیل یا شماره موبایل</label>
|
||||
<div class="input-wrap">
|
||||
@@ -147,20 +192,42 @@ function backToCredentials() {
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn submit" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال ارسال کد…' : 'ورود' }}
|
||||
{{ loading ? 'در حال ورود…' : 'ورود' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else-if="otpStep === 'mobile'">
|
||||
<form class="form" @submit.prevent="submitOtpMobile">
|
||||
<div class="field">
|
||||
<label for="mobile">شماره موبایل</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="mobile"
|
||||
v-model="mobile"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel"
|
||||
placeholder="۰۹۱۲۱۲۳۴۵۶۷"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico user" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn submit" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال ارسال کد…' : 'ارسال کد' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<button type="button" class="back" @click="backToCredentials">بازگشت</button>
|
||||
<h1>تأیید هویت</h1>
|
||||
<p class="lead">{{ otpHint }}</p>
|
||||
<button type="button" class="back" @click="backToOtpMobile">بازگشت</button>
|
||||
<p class="otp-lead">{{ otpHint }}</p>
|
||||
<p v-if="challenge?.devCode" class="dev-hint">
|
||||
کد توسعه: <strong>{{ challenge.devCode }}</strong>
|
||||
</p>
|
||||
|
||||
<form class="form" @submit.prevent="submitOtp">
|
||||
<form class="form" @submit.prevent="submitOtpCode">
|
||||
<div class="field">
|
||||
<label for="otp">کد یکبارمصرف</label>
|
||||
<div class="input-wrap">
|
||||
@@ -272,11 +339,38 @@ h1 {
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0 0 1.4rem;
|
||||
margin: 0 0 1rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 1.1rem;
|
||||
padding: 0.3rem;
|
||||
border-radius: 14px;
|
||||
background: color-mix(in srgb, var(--line) 55%, white);
|
||||
}
|
||||
|
||||
.mode-tabs button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 11px;
|
||||
padding: 0.65rem 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.mode-tabs button.on {
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 0.95rem;
|
||||
@@ -392,8 +486,14 @@ h1 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.otp-lead {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.dev-hint {
|
||||
margin: -0.6rem 0 1rem;
|
||||
margin: -0.4rem 0 1rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-soft);
|
||||
|
||||
@@ -81,9 +81,9 @@ async function submitOtp() {
|
||||
const mobile = mobileCode.value.trim();
|
||||
const email =
|
||||
challenge.value.emailOtpRequired === false
|
||||
? mobile
|
||||
? undefined
|
||||
: emailCode.value.trim();
|
||||
await auth.verifyRegister(challenge.value.challengeId, email, mobile);
|
||||
await auth.verifyRegister(challenge.value.challengeId, mobile, email);
|
||||
await router.push('/');
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
||||
|
||||
Reference in New Issue
Block a user