Improve auth UX and password strength across dashboards.

Remove business self-registration, localize SMS/auth errors, add password strength + confirm on reset/signup, and brand customer role as website user. Also include websites SSL sync UI updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 23:29:55 +03:30
co-authored by Cursor
parent 656fc57494
commit aeaad0f645
22 changed files with 847 additions and 285 deletions
@@ -104,6 +104,10 @@
color: var(--primary);
}
:global([dir='rtl']) .backBtn svg {
transform: scaleX(-1);
}
.form {
display: flex;
flex-direction: column;
@@ -152,6 +156,11 @@
box-sizing: border-box;
}
.inputWrap input[dir='ltr'] {
direction: ltr;
text-align: left;
}
.inputWrap input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);
@@ -195,6 +204,7 @@
font-size: 15px;
font-weight: 600;
color: white;
text-align: center;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: var(--radius-sm);
box-shadow: 0 4px 14px rgba(var(--primary-rgb) / 0.35);
@@ -202,6 +212,10 @@
margin-top: 4px;
}
button.submitBtn {
text-align: center;
}
.submitBtn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(var(--primary-rgb) / 0.4);
@@ -246,11 +260,20 @@
.footerText {
margin-top: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
text-align: center;
font-size: 14px;
color: var(--text-secondary);
}
p.footerText {
text-align: center;
}
.codeHint {
font-size: 13px;
color: var(--text-secondary);
+144 -18
View File
@@ -15,7 +15,7 @@ import {
resetPassword,
sendOtp,
} from '../services/authService'
import { LanguageSelect } from '@meshkee/dashboard-ui'
import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong } from '@meshkee/dashboard-ui'
import { useT } from '../i18n/useT'
import meshkeeLogo from '../assets/meshkee-logo.png'
import styles from './LoginPage.module.css'
@@ -49,6 +49,7 @@ export function LoginPage() {
const [lastName, setLastName] = useState('')
const [smsCode, setSmsCode] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmNewPassword, setConfirmNewPassword] = useState('')
const [codeSent, setCodeSent] = useState(false)
const [countdown, setCountdown] = useState(0)
const [error, setError] = useState('')
@@ -68,6 +69,7 @@ export function LoginPage() {
setLastName('')
setSmsCode('')
setNewPassword('')
setConfirmNewPassword('')
setSmsStep('phone')
setCodeSent(false)
setShowPassword(false)
@@ -94,19 +96,50 @@ export function LoginPage() {
function handleApiError(err: unknown, fallback: string) {
if (err instanceof ApiError) {
setError(err.message)
if (
err.message.includes('E.164') ||
err.message.includes('cellNumber must be')
) {
setError(t('login.error.cellEnglishDigits'))
} else if (
err.message.includes('SMS provider is not configured') ||
err.message.includes('SMS provider is unreachable') ||
err.message.includes('SMS provider returned') ||
err.message.includes('Unable to send SMS')
) {
setError(t('login.error.smsProvider'))
} else if (err.message.includes('Cell number exists on another account')) {
setError(t('signup.error.cellExists'))
} else {
setError(err.message)
}
} else {
setError(fallback)
}
}
function resolveCellNumber(): string | null {
if (/[۰-۹٠-٩]/.test(phone)) {
setError(t('login.error.cellEnglishDigits'))
return null
}
const cellNumber = toE164CellNumber(phone)
if (!/^\+[1-9]\d{6,14}$/.test(cellNumber)) {
setError(t('login.error.cellEnglishDigits'))
return null
}
return cellNumber
}
async function handleSendCode() {
clearMessages()
const cellNumber = resolveCellNumber()
if (!cellNumber) return
setIsSubmitting(true)
try {
const cellNumber = toE164CellNumber(phone)
const result = await sendOtp(cellNumber)
const result = await sendOtp(cellNumber, tenantDomain)
if (!result.enabled) {
setInfo(result.message)
@@ -125,10 +158,12 @@ export function LoginPage() {
async function handleLogin(e: React.FormEvent) {
e.preventDefault()
clearMessages()
const cellNumber = resolveCellNumber()
if (!cellNumber) return
setIsSubmitting(true)
try {
const cellNumber = toE164CellNumber(phone)
await login(cellNumber, password)
navigate(redirectTo)
} catch (err) {
@@ -147,15 +182,17 @@ export function LoginPage() {
return
}
if (password.length < 8) {
setError(t('signup.error.length'))
if (!isPasswordStrong(password)) {
setError(t('forgot.error.strength'))
return
}
const cellNumber = resolveCellNumber()
if (!cellNumber) return
setIsSubmitting(true)
try {
const cellNumber = toE164CellNumber(phone)
const data = await register({
cellNumber,
password,
@@ -179,7 +216,7 @@ export function LoginPage() {
setSmsCode('')
setInfo(t('signup.info.verify'))
try {
const otpResult = await sendOtp(cellNumber)
const otpResult = await sendOtp(cellNumber, tenantDomain)
if (!otpResult.enabled) {
setInfo(otpResult.message)
}
@@ -205,15 +242,22 @@ export function LoginPage() {
e.preventDefault()
clearMessages()
if (newPassword.length < 8) {
setError(t('forgot.error.length'))
if (!isPasswordStrong(newPassword)) {
setError(t('forgot.error.strength'))
return
}
if (newPassword !== confirmNewPassword) {
setError(t('forgot.error.match'))
return
}
const cellNumber = resolveCellNumber()
if (!cellNumber) return
setIsSubmitting(true)
try {
const cellNumber = toE164CellNumber(phone)
await resetPassword(cellNumber, smsCode, newPassword)
setInfo(t('forgot.info.success'))
setTimeout(() => switchView('login'), 2000)
@@ -227,10 +271,12 @@ export function LoginPage() {
async function handleOtpLogin(e: React.FormEvent) {
e.preventDefault()
clearMessages()
const cellNumber = resolveCellNumber()
if (!cellNumber) return
setIsSubmitting(true)
try {
const cellNumber = toE164CellNumber(phone)
await loginWithOtp(cellNumber, smsCode)
navigate(redirectTo)
} catch (err) {
@@ -291,6 +337,8 @@ export function LoginPage() {
<input
id="login-password"
type={showPassword ? 'text' : 'password'}
dir="ltr"
lang="en"
placeholder={t('login.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -341,7 +389,7 @@ export function LoginPage() {
</button>
<p className={styles.footerText}>
{t('login.noAccount')}{' '}
<span>{t('login.noAccount')}</span>
<button
type="button"
className={styles.linkBtn}
@@ -357,7 +405,6 @@ export function LoginPage() {
{view === 'signup' && (
<>
<h1 className={styles.title}>{t('signup.title')}</h1>
<p className={styles.subtitle}>{t('signup.subtitle', { domain: tenantDomain })}</p>
<form className={styles.form} onSubmit={handleSignup}>
{error && (
@@ -426,6 +473,8 @@ export function LoginPage() {
<input
id="signup-password"
type={showPassword ? 'text' : 'password'}
dir="ltr"
lang="en"
placeholder={t('signup.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -443,6 +492,24 @@ export function LoginPage() {
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<PasswordStrengthMeter
password={password}
id="signup-password-strength"
labels={{
title: t('password.strength'),
empty: t('password.strength.empty'),
weak: t('password.strength.weak'),
fair: t('password.strength.fair'),
good: t('password.strength.good'),
strong: t('password.strength.strong'),
rules: {
length: t('password.rule.length'),
upper: t('password.rule.upper'),
number: t('password.rule.number'),
special: t('password.rule.special'),
},
}}
/>
</div>
<div className={styles.field}>
@@ -452,6 +519,8 @@ export function LoginPage() {
<input
id="signup-confirm"
type={showPassword ? 'text' : 'password'}
dir="ltr"
lang="en"
placeholder={t('signup.confirmPlaceholder')}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
@@ -462,13 +531,17 @@ export function LoginPage() {
</div>
</div>
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
<button
type="submit"
className={styles.submitBtn}
disabled={isSubmitting || !isPasswordStrong(password)}
>
{isSubmitting ? t('signup.creating') : t('signup.create')}
</button>
</form>
<p className={styles.footerText}>
{t('signup.hasAccount')}{' '}
<span>{t('signup.hasAccount')}</span>
<button
type="button"
className={styles.linkBtn}
@@ -567,12 +640,61 @@ export function LoginPage() {
<input
id="forgot-new-password"
type={showPassword ? 'text' : 'password'}
dir="ltr"
lang="en"
placeholder={t('forgot.newPasswordPlaceholder')}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
disabled={isSubmitting}
aria-describedby="forgot-password-strength"
/>
<button
type="button"
className={styles.togglePassword}
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? t('login.hidePassword') : t('login.showPassword')}
disabled={isSubmitting}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<PasswordStrengthMeter
password={newPassword}
id="forgot-password-strength"
labels={{
title: t('password.strength'),
empty: t('password.strength.empty'),
weak: t('password.strength.weak'),
fair: t('password.strength.fair'),
good: t('password.strength.good'),
strong: t('password.strength.strong'),
rules: {
length: t('password.rule.length'),
upper: t('password.rule.upper'),
number: t('password.rule.number'),
special: t('password.rule.special'),
},
}}
/>
</div>
<div className={styles.field}>
<label htmlFor="forgot-confirm-password">{t('forgot.confirm')}</label>
<div className={styles.inputWrap}>
<Lock size={18} className={styles.inputIcon} />
<input
id="forgot-confirm-password"
type={showPassword ? 'text' : 'password'}
dir="ltr"
lang="en"
placeholder={t('forgot.confirmPlaceholder')}
value={confirmNewPassword}
onChange={(e) => setConfirmNewPassword(e.target.value)}
required
minLength={8}
disabled={isSubmitting}
/>
</div>
</div>
@@ -592,7 +714,11 @@ export function LoginPage() {
)}
</div>
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
<button
type="submit"
className={styles.submitBtn}
disabled={isSubmitting || !isPasswordStrong(newPassword)}
>
{isSubmitting ? t('forgot.verifying') : t('forgot.reset')}
</button>
</>
@@ -36,7 +36,15 @@ export function CheckoutLoginStep() {
setIsSubmitting(true)
try {
if (/[۰-۹٠-٩]/.test(phone)) {
setError('خواهشمندیم از زبان انگلیسی برای شماره همراه استفاده کنید')
return
}
const cellNumber = toE164CellNumber(phone)
if (!/^\+[1-9]\d{6,14}$/.test(cellNumber)) {
setError('خواهشمندیم از زبان انگلیسی برای شماره همراه استفاده کنید')
return
}
await login(cellNumber, password)
try {
await syncGuestCartToServer()
@@ -45,7 +53,12 @@ export function CheckoutLoginStep() {
}
navigate(resolveRedirectPath(), { replace: true })
} catch (err) {
setError(err instanceof ApiError ? err.message : 'ورود ممکن نشد.')
const message = err instanceof ApiError ? err.message : 'ورود ممکن نشد.'
setError(
message.includes('E.164') || message.includes('cellNumber must be')
? 'خواهشمندیم از زبان انگلیسی برای شماره همراه استفاده کنید'
: message,
)
} finally {
setIsSubmitting(false)
}
@@ -92,6 +105,8 @@ export function CheckoutLoginStep() {
<input
id="checkout-password"
type={showPassword ? 'text' : 'password'}
dir="ltr"
lang="en"
placeholder="رمز عبور خود را وارد کنید"
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -772,6 +772,11 @@
transition: border-color 0.2s, box-shadow 0.2s;
}
.inputWrap input[dir='ltr'] {
direction: ltr;
text-align: left;
}
.inputWrap input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);