Règle de gestion des mots de passe
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* PasswordStrength — indicateur de complexité de mot de passe.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* password : string — valeur en cours de saisie
|
||||||
|
*
|
||||||
|
* Affiche uniquement quand password est non vide.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RULES = [
|
||||||
|
{ id: 'len', label: '8 caractères minimum', test: p => p.length >= 8 },
|
||||||
|
{ id: 'upper', label: 'Une lettre majuscule (A–Z)', test: p => /[A-Z]/.test(p) },
|
||||||
|
{ id: 'lower', label: 'Une lettre minuscule (a–z)', test: p => /[a-z]/.test(p) },
|
||||||
|
{ id: 'digit', label: 'Un chiffre (0–9)', test: p => /[0-9]/.test(p) },
|
||||||
|
{ id: 'special', label: 'Un caractère spécial (!@#…)', test: p => /[^A-Za-z0-9]/.test(p) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LEVELS = [
|
||||||
|
{ min: 0, label: 'Très faible', color: '#ef4444' },
|
||||||
|
{ min: 1, label: 'Faible', color: '#f97316' },
|
||||||
|
{ min: 2, label: 'Moyen', color: '#eab308' },
|
||||||
|
{ min: 3, label: 'Bon', color: '#84cc16' },
|
||||||
|
{ min: 4, label: 'Fort', color: '#22c55e' },
|
||||||
|
{ min: 5, label: 'Très fort', color: '#10b981' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getLevel(score) {
|
||||||
|
// score = nombre de règles respectées (0..5)
|
||||||
|
for (let i = LEVELS.length - 1; i >= 0; i--) {
|
||||||
|
if (score >= LEVELS[i].min) return LEVELS[i];
|
||||||
|
}
|
||||||
|
return LEVELS[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PasswordStrength({ password }) {
|
||||||
|
if (!password) return null;
|
||||||
|
|
||||||
|
const results = RULES.map(r => ({ ...r, ok: r.test(password) }));
|
||||||
|
const score = results.filter(r => r.ok).length;
|
||||||
|
const level = getLevel(score);
|
||||||
|
const pct = Math.round((score / RULES.length) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
{/* Barre de progression */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
|
||||||
|
<div style={{
|
||||||
|
flex: 1, height: 5, borderRadius: 3,
|
||||||
|
background: 'var(--border, #e5e7eb)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
width: `${pct}%`,
|
||||||
|
background: level.color,
|
||||||
|
borderRadius: 3,
|
||||||
|
transition: 'width .25s ease, background .25s ease',
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap',
|
||||||
|
color: level.color,
|
||||||
|
minWidth: 64, textAlign: 'right',
|
||||||
|
transition: 'color .25s ease',
|
||||||
|
}}>
|
||||||
|
{level.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des règles */}
|
||||||
|
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
{results.map(r => (
|
||||||
|
<li key={r.id} style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 7,
|
||||||
|
fontSize: 12,
|
||||||
|
color: r.ok ? '#16a34a' : 'var(--text-muted)',
|
||||||
|
transition: 'color .2s ease',
|
||||||
|
}}>
|
||||||
|
{r.ok ? (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{r.label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
|
|
||||||
// Wrap défini en dehors du composant pour éviter le remontage à chaque frappe
|
// Wrap défini en dehors du composant pour éviter le remontage à chaque frappe
|
||||||
@@ -173,6 +174,7 @@ export default function InvitationRegister() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(8 car. min.)</span></label>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(8 car. min.)</span></label>
|
||||||
<input className="form-input" type="password" required minLength={8} autoComplete="new-password" placeholder="••••••••" value={form.password} onChange={e => set('password', e.target.value)} style={{ width: '100%' }} />
|
<input className="form-input" type="password" required minLength={8} autoComplete="new-password" placeholder="••••••••" value={form.password} onChange={e => set('password', e.target.value)} style={{ width: '100%' }} />
|
||||||
|
<PasswordStrength password={form.password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import { useUi } from '../context/UiContext.jsx';
|
import { useUi } from '../context/UiContext.jsx';
|
||||||
@@ -470,6 +471,7 @@ function SecurityForm() {
|
|||||||
<input type="password" required autoComplete="new-password"
|
<input type="password" required autoComplete="new-password"
|
||||||
value={pwdForm.newPassword}
|
value={pwdForm.newPassword}
|
||||||
onChange={e => setPwdForm({ ...pwdForm, newPassword: e.target.value })} />
|
onChange={e => setPwdForm({ ...pwdForm, newPassword: e.target.value })} />
|
||||||
|
<PasswordStrength password={pwdForm.newPassword} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Confirmer le nouveau mot de passe</label>
|
<label>Confirmer le nouveau mot de passe</label>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
@@ -205,7 +206,7 @@ export default function Register() {
|
|||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||||
Mot de passe <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(8 car. min.)</span>
|
Mot de passe
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input"
|
||||||
@@ -217,6 +218,7 @@ export default function Register() {
|
|||||||
onChange={set('password')}
|
onChange={set('password')}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
|
<PasswordStrength password={form.password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
|
|
||||||
export default function ResetPassword() {
|
export default function ResetPassword() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
@@ -192,6 +193,7 @@ export default function ResetPassword() {
|
|||||||
onChange={e => { setPassword(e.target.value); setStatus(null); }}
|
onChange={e => { setPassword(e.target.value); setStatus(null); }}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
|
<PasswordStrength password={password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { api } from '../../api.js';
|
|||||||
import ConfirmModal from '../../components/ConfirmModal.jsx';
|
import ConfirmModal from '../../components/ConfirmModal.jsx';
|
||||||
import Modal from '../../components/Modal.jsx';
|
import Modal from '../../components/Modal.jsx';
|
||||||
import ResultBanner from '../../components/ResultBanner.jsx';
|
import ResultBanner from '../../components/ResultBanner.jsx';
|
||||||
|
import PasswordStrength from '../../components/PasswordStrength.jsx';
|
||||||
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
@@ -356,6 +357,7 @@ function CreateUserModal({ open, onClose, onCreated }) {
|
|||||||
<div>
|
<div>
|
||||||
<label>Mot de passe *</label>
|
<label>Mot de passe *</label>
|
||||||
<input type="password" required minLength={8} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder="8 caractères minimum" />
|
<input type="password" required minLength={8} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder="8 caractères minimum" />
|
||||||
|
<PasswordStrength password={form.password} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Rôle</label>
|
<label>Rôle</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user