mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
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:
co-authored by
Cursor
parent
656fc57494
commit
aeaad0f645
@@ -1,23 +1,17 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Eye, EyeOff, Smartphone, Lock, KeyRound, ArrowLeft, User } from 'lucide-react'
|
||||
import { LanguageSelect } from '@meshkee/dashboard-ui'
|
||||
import { Eye, EyeOff, Smartphone, Lock, KeyRound, ArrowLeft } from 'lucide-react'
|
||||
import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong } from '@meshkee/dashboard-ui'
|
||||
import { useAuth, BUSINESS_ACCESS_MESSAGE } from '../context/AuthContext'
|
||||
import { useTenantBranding } from '../context/TenantBrandingContext'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { toE164CellNumber } from '../lib/cellNumber'
|
||||
import { getBusinessDomain } from '../lib/config'
|
||||
import { setActiveBusiness } from '../lib/businessContext'
|
||||
import {
|
||||
logout as logoutRequest,
|
||||
register,
|
||||
resetPassword,
|
||||
sendOtp,
|
||||
} from '../services/authService'
|
||||
import { resetPassword, sendOtp } from '../services/authService'
|
||||
import { useT } from '../i18n/useT'
|
||||
import styles from './LoginPage.module.css'
|
||||
|
||||
type AuthView = 'login' | 'signup' | 'forgot' | 'otp'
|
||||
type AuthView = 'login' | 'forgot' | 'otp'
|
||||
type SmsStep = 'phone' | 'code'
|
||||
|
||||
export function LoginPage() {
|
||||
@@ -32,11 +26,9 @@ export function LoginPage() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [phone, setPhone] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [firstName, setFirstName] = useState('')
|
||||
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('')
|
||||
@@ -51,11 +43,9 @@ export function LoginPage() {
|
||||
function resetForm() {
|
||||
setPhone('')
|
||||
setPassword('')
|
||||
setConfirmPassword('')
|
||||
setFirstName('')
|
||||
setLastName('')
|
||||
setSmsCode('')
|
||||
setNewPassword('')
|
||||
setConfirmNewPassword('')
|
||||
setSmsStep('phone')
|
||||
setCodeSent(false)
|
||||
setShowPassword(false)
|
||||
@@ -84,6 +74,18 @@ export function LoginPage() {
|
||||
if (err instanceof ApiError) {
|
||||
if (err.message === BUSINESS_ACCESS_MESSAGE) {
|
||||
setError(t('login.error.access'))
|
||||
} else 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 {
|
||||
setError(err.message)
|
||||
}
|
||||
@@ -92,13 +94,28 @@ export function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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, businessDomain)
|
||||
|
||||
if (!result.enabled) {
|
||||
setInfo(result.message)
|
||||
@@ -117,10 +134,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('/')
|
||||
} catch (err) {
|
||||
@@ -130,60 +149,26 @@ export function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSignup(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
clearMessages()
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError(t('signup.error.match'))
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError(t('signup.error.length'))
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const cellNumber = toE164CellNumber(phone)
|
||||
const data = await register({
|
||||
cellNumber,
|
||||
password,
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
domain: businessDomain,
|
||||
})
|
||||
|
||||
if (data.user.dashboard !== 'business' || data.user.businesses.length === 0) {
|
||||
logoutRequest()
|
||||
setError(t('login.error.access'))
|
||||
return
|
||||
}
|
||||
|
||||
setActiveBusiness(data.user)
|
||||
navigate('/')
|
||||
} catch (err) {
|
||||
handleApiError(err, t('signup.error.create'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResetPassword(e: React.FormEvent) {
|
||||
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)
|
||||
@@ -197,10 +182,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('/')
|
||||
} catch (err) {
|
||||
@@ -261,6 +248,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)}
|
||||
@@ -309,144 +298,6 @@ export function LoginPage() {
|
||||
<KeyRound size={18} />
|
||||
{t('login.otp')}
|
||||
</button>
|
||||
|
||||
<p className={styles.footerText}>
|
||||
{t('login.noAccount')}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.linkBtn}
|
||||
onClick={() => switchView('signup')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('login.signUp')}
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{view === 'signup' && (
|
||||
<>
|
||||
<h1 className={styles.title}>{t('signup.title')}</h1>
|
||||
<p className={styles.subtitle}>{t('signup.subtitle', { domain: businessDomain })}</p>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSignup}>
|
||||
{error && (
|
||||
<div className={styles.error} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.fieldRow}>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-first">{t('signup.firstName')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<User size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-first"
|
||||
type="text"
|
||||
placeholder={t('signup.firstName')}
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
required
|
||||
minLength={2}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-last">{t('signup.lastName')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<User size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-last"
|
||||
type="text"
|
||||
placeholder={t('signup.lastName')}
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
required
|
||||
minLength={2}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-phone">{t('login.mobile')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Smartphone size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-phone"
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
placeholder="09123456789"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-password">{t('login.password')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('signup.passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-confirm">{t('signup.confirm')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-confirm"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('signup.confirmPlaceholder')}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
|
||||
{isSubmitting ? t('signup.creating') : t('signup.create')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className={styles.footerText}>
|
||||
{t('signup.hasAccount')}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.linkBtn}
|
||||
onClick={() => switchView('login')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('signup.signIn')}
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -534,12 +385,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>
|
||||
@@ -561,7 +461,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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user