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
@@ -71,6 +71,11 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field input[dir='ltr'] {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);
|
||||
|
||||
@@ -192,6 +192,8 @@ export function PasswordResetModal({
|
||||
<input
|
||||
id="password-reset-current"
|
||||
type="password"
|
||||
dir="ltr"
|
||||
lang="en"
|
||||
name="current-password-field"
|
||||
autoComplete="current-password"
|
||||
value={currentPassword}
|
||||
@@ -206,6 +208,8 @@ export function PasswordResetModal({
|
||||
<input
|
||||
id="password-reset-new"
|
||||
type="password"
|
||||
dir="ltr"
|
||||
lang="en"
|
||||
name="new-password-field"
|
||||
autoComplete="new-password"
|
||||
value={newPassword}
|
||||
@@ -255,6 +259,8 @@ export function PasswordResetModal({
|
||||
<input
|
||||
id="password-reset-confirm"
|
||||
type="password"
|
||||
dir="ltr"
|
||||
lang="en"
|
||||
name="confirm-password-field"
|
||||
autoComplete="new-password"
|
||||
value={confirmPassword}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
.strength {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.strengthHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.strengthTitle {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.strengthLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tone_empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.tone_weak {
|
||||
color: #dc2626;
|
||||
}
|
||||
.tone_fair {
|
||||
color: #d97706;
|
||||
}
|
||||
.tone_good {
|
||||
color: #2563eb;
|
||||
}
|
||||
.tone_strong {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.strengthTrack {
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.28);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.strengthFill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
transition: width 0.22s ease, background-color 0.22s ease;
|
||||
}
|
||||
|
||||
.fill_empty {
|
||||
background: transparent;
|
||||
}
|
||||
.fill_weak {
|
||||
background: #ef4444;
|
||||
}
|
||||
.fill_fair {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.fill_good {
|
||||
background: #3b82f6;
|
||||
}
|
||||
.fill_strong {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.rules {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.ruleMet {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.ruleDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(148, 163, 184, 0.55);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ruleMet .ruleDot {
|
||||
background: #22c55e;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import styles from './PasswordStrengthMeter.module.css'
|
||||
|
||||
const PASSWORD_RULES = [
|
||||
{
|
||||
id: 'length',
|
||||
test: (value: string) => value.length >= 8,
|
||||
},
|
||||
{
|
||||
id: 'upper',
|
||||
test: (value: string) => /[A-Z]/.test(value),
|
||||
},
|
||||
{
|
||||
id: 'number',
|
||||
test: (value: string) => /\d/.test(value),
|
||||
},
|
||||
{
|
||||
id: 'special',
|
||||
test: (value: string) => /[^A-Za-z0-9]/.test(value),
|
||||
},
|
||||
] as const
|
||||
|
||||
export type PasswordRuleId = (typeof PASSWORD_RULES)[number]['id']
|
||||
export type StrengthTone = 'empty' | 'weak' | 'fair' | 'good' | 'strong'
|
||||
|
||||
export type PasswordStrengthLabels = {
|
||||
title: string
|
||||
empty: string
|
||||
weak: string
|
||||
fair: string
|
||||
good: string
|
||||
strong: string
|
||||
rules: Record<PasswordRuleId, string>
|
||||
}
|
||||
|
||||
const DEFAULT_LABELS: PasswordStrengthLabels = {
|
||||
title: 'Password strength',
|
||||
empty: 'Enter a password',
|
||||
weak: 'Weak',
|
||||
fair: 'Fair',
|
||||
good: 'Good',
|
||||
strong: 'Strong',
|
||||
rules: {
|
||||
length: 'At least 8 characters',
|
||||
upper: 'One uppercase letter',
|
||||
number: 'One number',
|
||||
special: 'One special character',
|
||||
},
|
||||
}
|
||||
|
||||
export function getPasswordChecks(password: string, labels: PasswordStrengthLabels = DEFAULT_LABELS) {
|
||||
return PASSWORD_RULES.map((rule) => ({
|
||||
id: rule.id,
|
||||
label: labels.rules[rule.id],
|
||||
met: rule.test(password),
|
||||
}))
|
||||
}
|
||||
|
||||
export function getStrengthMeta(
|
||||
metCount: number,
|
||||
hasInput: boolean,
|
||||
labels: PasswordStrengthLabels = DEFAULT_LABELS,
|
||||
): {
|
||||
tone: StrengthTone
|
||||
label: string
|
||||
percent: number
|
||||
} {
|
||||
if (!hasInput || metCount === 0) {
|
||||
return { tone: 'empty', label: labels.empty, percent: 0 }
|
||||
}
|
||||
if (metCount === 1) return { tone: 'weak', label: labels.weak, percent: 25 }
|
||||
if (metCount === 2) return { tone: 'fair', label: labels.fair, percent: 50 }
|
||||
if (metCount === 3) return { tone: 'good', label: labels.good, percent: 75 }
|
||||
return { tone: 'strong', label: labels.strong, percent: 100 }
|
||||
}
|
||||
|
||||
export function isPasswordStrong(password: string): boolean {
|
||||
return PASSWORD_RULES.every((rule) => rule.test(password))
|
||||
}
|
||||
|
||||
export function getPasswordValidationError(
|
||||
password: string,
|
||||
labels: PasswordStrengthLabels = DEFAULT_LABELS,
|
||||
): string | null {
|
||||
const unmet = PASSWORD_RULES.filter((rule) => !rule.test(password))
|
||||
if (unmet.length === 0) return null
|
||||
return unmet.map((rule) => labels.rules[rule.id]).join(', ')
|
||||
}
|
||||
|
||||
export interface PasswordStrengthMeterProps {
|
||||
password: string
|
||||
labels?: PasswordStrengthLabels
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function PasswordStrengthMeter({
|
||||
password,
|
||||
labels = DEFAULT_LABELS,
|
||||
id = 'password-strength',
|
||||
}: PasswordStrengthMeterProps) {
|
||||
const checks = getPasswordChecks(password, labels)
|
||||
const metCount = checks.filter((check) => check.met).length
|
||||
const strength = getStrengthMeta(metCount, password.length > 0, labels)
|
||||
|
||||
return (
|
||||
<div className={styles.strength} id={id}>
|
||||
<div className={styles.strengthHeader}>
|
||||
<span className={styles.strengthTitle}>{labels.title}</span>
|
||||
<span className={`${styles.strengthLabel} ${styles[`tone_${strength.tone}`]}`}>
|
||||
{strength.label}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.strengthTrack}
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={strength.percent}
|
||||
aria-label={labels.title}
|
||||
>
|
||||
<div
|
||||
className={`${styles.strengthFill} ${styles[`fill_${strength.tone}`]}`}
|
||||
style={{ width: `${strength.percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<ul className={styles.rules} id={`${id}-rules`}>
|
||||
{checks.map((check) => (
|
||||
<li key={check.id} className={`${styles.rule} ${check.met ? styles.ruleMet : ''}`}>
|
||||
<span className={styles.ruleDot} aria-hidden="true" />
|
||||
{check.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,15 @@ export {
|
||||
type ChangePasswordHandler,
|
||||
type PasswordResetModalProps,
|
||||
} from './components/PasswordResetModal'
|
||||
export {
|
||||
PasswordStrengthMeter,
|
||||
getPasswordChecks,
|
||||
getPasswordValidationError,
|
||||
getStrengthMeta,
|
||||
isPasswordStrong,
|
||||
type PasswordStrengthLabels,
|
||||
type PasswordStrengthMeterProps,
|
||||
} from './components/PasswordStrengthMeter'
|
||||
export { LanguageSelect } from './components/LanguageSelect'
|
||||
export { LocaleProvider, useLocale } from './context/LocaleContext'
|
||||
export { useDashboardDocumentTitle } from './hooks/useDashboardDocumentTitle'
|
||||
|
||||
Reference in New Issue
Block a user