Multi-environment and multi-user construction
This commit is contained in:
@@ -5,7 +5,8 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
|
||||
import MentoLogo from '@/components/brand/MentoLogo.vue';
|
||||
import axios from 'axios';
|
||||
import type { LoginChallengeResponse } from '@mento/shared';
|
||||
import type { LoginChallengeResponse, LoginOrganizationChoiceResponse } from '@mento/shared';
|
||||
import { USER_ROLE_LABELS } from '@/lib/roles';
|
||||
import { brand } from '@mento/shared';
|
||||
import BrandName from '@/components/brand/BrandName.vue';
|
||||
|
||||
@@ -24,6 +25,7 @@ const showPassword = ref(false);
|
||||
const remember = ref(true);
|
||||
const otpCode = ref('');
|
||||
const challenge = ref<LoginChallengeResponse | null>(null);
|
||||
const pendingChoice = ref<LoginOrganizationChoiceResponse | null>(null);
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -48,12 +50,20 @@ async function goHome() {
|
||||
await router.push(redirect);
|
||||
}
|
||||
|
||||
async function afterAuth(result: LoginOrganizationChoiceResponse | void) {
|
||||
if (result) {
|
||||
pendingChoice.value = result;
|
||||
return;
|
||||
}
|
||||
await goHome();
|
||||
}
|
||||
|
||||
async function submitPasswordLogin() {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.login(identifier.value.trim(), password.value);
|
||||
await goHome();
|
||||
const result = await auth.login(identifier.value.trim(), password.value);
|
||||
await afterAuth(result);
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ورود ناموفق بود');
|
||||
} finally {
|
||||
@@ -80,8 +90,8 @@ async function submitOtpCode() {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.verifyLoginOtp(challenge.value.challengeId, otpCode.value.trim());
|
||||
await goHome();
|
||||
const result = await auth.verifyLoginOtp(challenge.value.challengeId, otpCode.value.trim());
|
||||
await afterAuth(result);
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
||||
} finally {
|
||||
@@ -103,6 +113,25 @@ function backToOtpMobile() {
|
||||
challenge.value = null;
|
||||
error.value = '';
|
||||
}
|
||||
|
||||
async function pickOrganization(organizationId: string) {
|
||||
if (!pendingChoice.value) return;
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.selectOrganization(pendingChoice.value.selectionToken, organizationId);
|
||||
await goHome();
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'انتخاب محیط ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOrgChoice() {
|
||||
pendingChoice.value = null;
|
||||
error.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -119,7 +148,33 @@ function backToOtpMobile() {
|
||||
|
||||
<div class="form-inner">
|
||||
<h1>خوش آمدید 👋</h1>
|
||||
<p class="lead">برای ورود به حساب کاربری خود، اطلاعاتتان را وارد کنید</p>
|
||||
<p class="lead">
|
||||
{{
|
||||
pendingChoice
|
||||
? 'در کدام محیط میخواهید وارد شوید؟'
|
||||
: 'برای ورود به حساب کاربری خود، اطلاعاتتان را وارد کنید'
|
||||
}}
|
||||
</p>
|
||||
|
||||
<template v-if="pendingChoice">
|
||||
<button type="button" class="back" @click="cancelOrgChoice">بازگشت</button>
|
||||
<div class="org-list">
|
||||
<button
|
||||
v-for="org in pendingChoice.organizations"
|
||||
:key="org.id"
|
||||
type="button"
|
||||
class="org-card"
|
||||
:disabled="loading"
|
||||
@click="pickOrganization(org.id)"
|
||||
>
|
||||
<strong>{{ org.name }}</strong>
|
||||
<span>{{ org.orgRoleName || USER_ROLE_LABELS[org.role] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div class="mode-tabs" role="tablist">
|
||||
<button
|
||||
@@ -257,6 +312,7 @@ function backToOtpMobile() {
|
||||
حساب کاربری ندارید؟
|
||||
<RouterLink to="/register">ثبتنام کنید</RouterLink>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -503,6 +559,36 @@ h1 {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.org-list {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.org-card {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
text-align: start;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 0.9rem 1rem;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.org-card:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent-soft) 55%, white);
|
||||
}
|
||||
|
||||
.org-card strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.org-card span {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
width: min(1040px, 100%);
|
||||
margin: 0.85rem auto 0;
|
||||
|
||||
@@ -296,8 +296,12 @@ function openInvite() {
|
||||
}
|
||||
|
||||
async function submitInvite() {
|
||||
if (!inviteName.value.trim() || !inviteMobile.value.trim() || invitePassword.value.length < 8) {
|
||||
error.value = 'نام، شماره موبایل و رمز حداقل ۸ کاراکتر لازم است.';
|
||||
if (!inviteName.value.trim() || !inviteMobile.value.trim()) {
|
||||
error.value = 'نام و شماره موبایل لازم است.';
|
||||
return;
|
||||
}
|
||||
if (invitePassword.value && invitePassword.value.length < 8) {
|
||||
error.value = 'رمز عبور در صورت ورود باید حداقل ۸ کاراکتر باشد.';
|
||||
return;
|
||||
}
|
||||
inviteSaving.value = true;
|
||||
@@ -306,12 +310,12 @@ async function submitInvite() {
|
||||
const payload: InviteUserRequest = {
|
||||
mobile: inviteMobile.value.trim(),
|
||||
fullName: inviteName.value.trim(),
|
||||
password: invitePassword.value,
|
||||
orgRoleId: inviteRoleId.value || undefined,
|
||||
...(invitePassword.value ? { password: invitePassword.value } : {}),
|
||||
};
|
||||
await api.post('/users', payload);
|
||||
inviteOpen.value = false;
|
||||
ok.value = 'کاربر جدید اضافه شد.';
|
||||
ok.value = 'کاربر به این محیط اضافه شد.';
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'افزودن کاربر ناموفق بود');
|
||||
@@ -826,7 +830,11 @@ onUnmounted(() => {
|
||||
<form class="form" @submit.prevent="submitInvite">
|
||||
<label>نام و نام خانوادگی<input v-model="inviteName" required minlength="2" /></label>
|
||||
<label>شماره موبایل<input v-model="inviteMobile" dir="ltr" inputmode="tel" placeholder="0912…" /></label>
|
||||
<label>رمز عبور<input v-model="invitePassword" type="password" minlength="8" /></label>
|
||||
<label>
|
||||
رمز عبور
|
||||
<input v-model="invitePassword" type="password" minlength="8" placeholder="فقط برای کاربر جدید" />
|
||||
</label>
|
||||
<p class="hint">اگر این شماره از قبل حساب دارد، بدون رمز به همین محیط اضافه میشود.</p>
|
||||
<label>
|
||||
نقش
|
||||
<select v-model="inviteRoleId">
|
||||
@@ -927,6 +935,7 @@ h1 {
|
||||
|
||||
.lead,
|
||||
.muted,
|
||||
.hint,
|
||||
.empty {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
|
||||
Reference in New Issue
Block a user