UX améliorations

This commit is contained in:
2026-07-03 19:02:44 +02:00
parent b481c4cc1d
commit e11c767c1c
3 changed files with 110 additions and 91 deletions
+4 -1
View File
@@ -71,9 +71,12 @@ const iconsDir2 = path.join(_dataDir, 'icons');
app.use('/api/icons-files', express.static(iconsDir2, { maxAge: '1h', fallthrough: false })); app.use('/api/icons-files', express.static(iconsDir2, { maxAge: '1h', fallthrough: false }));
// Basic rate-limit on auth endpoints // Basic rate-limit on auth endpoints
// Seuil relevé (50 → 300) : usage local mono-utilisateur/multi-investisseur avec
// beaucoup d'allers-retours de test (login, 2FA, création de comptes admin/invitation…),
// l'ancien seuil de 50 req/15min était atteint trop facilement en usage normal.
const authLimiter = rateLimit({ const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 50, max: 300,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
}); });
+66 -61
View File
@@ -43,7 +43,7 @@ function ProfileSelect({ label, options, value, onChange }) {
return ( return (
<div ref={ref} className="profile-field"> <div ref={ref} className="profile-field">
<span className="profile-label">{label}</span> <label>{label}</label>
<div className={`profile-select-trigger${open ? ' open' : ''}`} <div className={`profile-select-trigger${open ? ' open' : ''}`}
onClick={() => setOpen(o => !o)} role="button" tabIndex={0} onClick={() => setOpen(o => !o)} role="button" tabIndex={0}
onKeyDown={e => e.key === 'Enter' && setOpen(o => !o)}> onKeyDown={e => e.key === 'Enter' && setOpen(o => !o)}>
@@ -132,25 +132,31 @@ function EmailChangeForm({ onDone }) {
}; };
return ( return (
<form onSubmit={submit} style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 12 }}> <form onSubmit={submit} style={{
marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)',
display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400,
}}>
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-muted)' }}> <p style={{ margin: 0, fontSize: 13, color: 'var(--text-muted)' }}>
Après le changement, un email de vérification sera envoyé à la nouvelle adresse. Après le changement, un email de vérification sera envoyé à la nouvelle adresse.
</p> </p>
{err && <div className="error">{err}</div>} {err && <div className="error">{err}</div>}
{msg && <div className="success-msg">{msg}</div>} {msg && <div className="success-msg">{msg}</div>}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div>
<label className="profile-label">Nouvelle adresse email</label> <label>Nouvelle adresse email</label>
<input className="profile-input" type="email" required value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="nouvelle@email.com" /> <input type="email" required value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="nouvelle@email.com" />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div>
<label className="profile-label">Mot de passe actuel (confirmation)</label> <label>Mot de passe actuel (confirmation)</label>
<PasswordInput className="profile-input" required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" /> <PasswordInput required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
</div> </div>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 12 }}>
<button type="submit" className="btn btn-primary" disabled={busy} style={{ fontSize: 13 }}> <button type="submit" className="primary" disabled={busy}>
{busy ? 'Enregistrement…' : 'Confirmer le changement'} {busy ? 'Enregistrement…' : 'Confirmer le changement'}
</button> </button>
<button type="button" className="btn btn-outline" onClick={() => onDone()} style={{ fontSize: 13 }}>Annuler</button> <button type="button" onClick={() => onDone()}
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler
</button>
</div> </div>
</form> </form>
); );
@@ -191,36 +197,39 @@ function AccountForm() {
const handleBlur = () => save(); const handleBlur = () => save();
return ( return (
<div className="profile-page"> <>
{/* ── Mon profil ──────────────────────────────────────── */} {/* ── Mon profil ──────────────────────────────────────── */}
<section className="profile-section"> <div className="card">
<h2 className="profile-section-title">Mon profil</h2> <h3 style={{ margin: '0 0 4px' }}>Mon profil</h3>
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
Vos informations personnelles et votre adresse email de connexion.
</p>
{infoErr && <div className="error" style={{ marginBottom: 12 }}>{infoErr}</div>} {infoErr && <div className="error" style={{ marginBottom: 12 }}>{infoErr}</div>}
{infoMsg && <div className="success-msg" style={{ marginBottom: 12 }}>{infoMsg}</div>} {infoMsg && <div className="success-msg" style={{ marginBottom: 12 }}>{infoMsg}</div>}
<div className="profile-grid-2"> <div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
<div className="profile-field"> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<span className="profile-label">Prénom</span> <div>
<input className="profile-input" value={prenom} <label>Prénom</label>
<input value={prenom}
onChange={e => setPrenom(e.target.value)} onChange={e => setPrenom(e.target.value)}
onBlur={handleBlur} onBlur={handleBlur}
placeholder="Prénom" /> placeholder="Prénom" />
</div> </div>
<div className="profile-field"> <div>
<span className="profile-label">Nom</span> <label>Nom</label>
<input className="profile-input" value={nom} <input value={nom}
onChange={e => setNom(e.target.value)} onChange={e => setNom(e.target.value)}
onBlur={handleBlur} onBlur={handleBlur}
placeholder="NOM" /> placeholder="NOM" />
</div> </div>
</div> </div>
<div className="profile-field profile-field-full"> <div>
<span className="profile-label">Mon email</span> <label>Mon email</label>
<div className="profile-email-row" style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span className="profile-email-value">{user?.email}</span> <span style={{ fontSize: 'var(--fs-base)', color: 'var(--text)' }}>{user?.email}</span>
{user?.email_verified {user?.email_verified
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)', border: '1px solid #bbf7d0' }}> Vérifié</span> ? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)', border: '1px solid #bbf7d0' }}> Vérifié</span>
: <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid #fcd34d' }}>Non vérifié</span> : <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid #fcd34d' }}>Non vérifié</span>
@@ -228,27 +237,33 @@ function AccountForm() {
</div> </div>
{!user?.email_verified && <EmailResendBlock email={user?.email} />} {!user?.email_verified && <EmailResendBlock email={user?.email} />}
</div> </div>
<div style={{ marginTop: 20 }}>
<button className="profile-manage-btn" type="button" onClick={() => setChangingEmail(v => !v)}>
{changingEmail ? 'Annuler' : 'Changer mon email'}
</button>
</div> </div>
{!changingEmail && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<button className="primary" type="button" onClick={() => setChangingEmail(true)}>
Changer mon email
</button>
</div>
)}
{changingEmail && <EmailChangeForm onDone={() => setChangingEmail(false)} />} {changingEmail && <EmailChangeForm onDone={() => setChangingEmail(false)} />}
</section>
{loading && <p className="text-muted" style={{ fontSize: 'var(--fs-sm)', marginTop: 12 }}>Enregistrement</p>}
</div>
{/* ── Préférences ─────────────────────────────────────── */} {/* ── Préférences ─────────────────────────────────────── */}
<section className="profile-section"> <div className="card" style={{ marginTop: 20 }}>
<h2 className="profile-section-title">Préférences</h2> <h3 style={{ margin: '0 0 4px' }}>Préférences</h3>
<div className="profile-grid-2"> <p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
Langue et devise utilisées dans l'application.
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, maxWidth: 400 }}>
<ProfileSelect label="Langue" options={LANGUES} value={langue} onChange={setLangue} /> <ProfileSelect label="Langue" options={LANGUES} value={langue} onChange={setLangue} />
<ProfileSelect label="Devise" options={DEVISES} value={devise} onChange={setDevise} /> <ProfileSelect label="Devise" options={DEVISES} value={devise} onChange={setDevise} />
</div> </div>
</section>
{loading && <p className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>Enregistrement</p>}
</div> </div>
</>
); );
} }
@@ -322,15 +337,19 @@ function TwoFASection({ user }) {
{/* ── État normal ── */} {/* ── État normal ── */}
{status === 'idle' && !enabled && ( {status === 'idle' && !enabled && (
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button className="primary" onClick={() => startSetup()} disabled={busy}> <button className="primary" onClick={() => startSetup()} disabled={busy}>
{busy ? 'Chargement' : 'Activer le 2FA'} {busy ? 'Chargement' : 'Activer le 2FA'}
</button> </button>
</div>
)} )}
{status === 'idle' && enabled && ( {status === 'idle' && enabled && (
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button className="btn btn-outline" style={{ color: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }} <button className="btn btn-outline" style={{ color: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }}
onClick={() => { setStatus('disabling'); setErr(null); setMsg(null); }}> onClick={() => { setStatus('disabling'); setErr(null); setMsg(null); }}>
Désactiver le 2FA Désactiver le 2FA
</button> </button>
</div>
)} )}
{/* ── Setup : QR code + vérification ── */} {/* ── Setup : QR code + vérification ── */}
@@ -374,11 +393,13 @@ function TwoFASection({ user }) {
{busy ? '…' : 'Confirmer'} {busy ? '…' : 'Confirmer'}
</button> </button>
</form> </form>
<div style={{ display: 'flex', justifyContent: 'flex-end', maxWidth: 320, marginTop: 12 }}>
<button type="button" onClick={() => { setStatus('idle'); setSetupData(null); setCode(''); setErr(null); }} <button type="button" onClick={() => { setStatus('idle'); setSetupData(null); setCode(''); setErr(null); }}
style={{ marginTop: 12, background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}> style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler Annuler
</button> </button>
</div> </div>
</div>
)} )}
{/* ── Désactivation : mot de passe ── */} {/* ── Désactivation : mot de passe ── */}
@@ -399,10 +420,12 @@ function TwoFASection({ user }) {
{busy ? '…' : 'Désactiver'} {busy ? '…' : 'Désactiver'}
</button> </button>
</div> </div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
<button type="button" onClick={() => { setStatus('idle'); setErr(null); setDisablePwd(''); }} <button type="button" onClick={() => { setStatus('idle'); setErr(null); setDisablePwd(''); }}
style={{ marginTop: 10, background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}> style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
Annuler Annuler
</button> </button>
</div>
</form> </form>
)} )}
</div> </div>
@@ -438,26 +461,6 @@ function SecurityForm() {
finally { setPwdLoading(false); } finally { setPwdLoading(false); }
}; };
const handleBackfillComptes = async () => {
setLoadingBackfill(true);
setErrorMsg(null);
setSuccessMsg(null);
try {
const { updated, total } = await api.post('/remboursements/backfill-comptes', {});
if (updated === 0) {
setSuccessMsg(`Aucun remboursement à corriger (${total} vérifié${total > 1 ? 's' : ''}).`);
} else {
setSuccessMsg(`${updated} remboursement${updated > 1 ? 's' : ''} mis à jour sur ${total} vérifié${total > 1 ? 's' : ''}.`);
}
setShowBackfillModal(false);
} catch (err) {
setErrorMsg(err.message || 'Une erreur est survenue.');
setShowBackfillModal(false);
} finally {
setLoadingBackfill(false);
}
};
return ( return (
<div className="card"> <div className="card">
<h3 style={{ margin: '0 0 4px' }}>Mot de passe</h3> <h3 style={{ margin: '0 0 4px' }}>Mot de passe</h3>
@@ -742,10 +745,12 @@ export default function MonCompte() {
))} ))}
</aside> </aside>
{/* ── Contenu ─────────────────────────────────────── */} {/* ── Contenu ─────────────────────────────────────── */}
<div className="account-content"> <div className="account-content account-content-center">
<div className="account-content-narrow">
{section === 'profil' && <AccountForm />} {section === 'profil' && <AccountForm />}
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>} {section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
</div> </div>
</div>
</div> </div>
); );
+11
View File
@@ -1338,6 +1338,17 @@ tr:hover td { background: var(--surface-2); }
padding: 24px 0 32px 28px; padding: 24px 0 32px 28px;
min-width: 0; min-width: 0;
} }
/* Variante pour les pages "compte" à faible densité de contenu (MonCompte) :
évite que les cards s'étirent sur toute la largeur d'un grand écran.
Centrage via flex (plus fiable qu'un margin:auto sur un enfant de grid). */
.account-content-center {
display: flex;
justify-content: center;
}
.account-content-narrow {
width: 100%;
max-width: 760px;
}
/* AccountForm interne — profil + préférences côte-à-côte */ /* AccountForm interne — profil + préférences côte-à-côte */
.profile-page { .profile-page {