UX améliorations
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user