Maj Section Général

This commit is contained in:
2026-06-15 23:03:37 +02:00
parent 01526a5551
commit 032a370e7d
15 changed files with 492 additions and 57 deletions
+208
View File
@@ -0,0 +1,208 @@
/**
* GeneralSection — Paramètres généraux de l'application (Admin).
* GET/PATCH /api/admin/general
*/
import { useState, useEffect } from 'react';
import { api } from '../../api.js';
function SettingRow({ label, description, children }) {
return (
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr',
gap: '12px 24px', alignItems: 'center',
padding: '18px 0', borderBottom: '1px solid var(--border)',
}}>
<div>
<div style={{ fontWeight: 600, color: 'var(--text)', marginBottom: description ? 4 : 0 }}>{label}</div>
{description && <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', lineHeight: 1.4 }}>{description}</div>}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>{children}</div>
</div>
);
}
function SectionHeader({ title, description }) {
return (
<div style={{ marginBottom: 28 }}>
<h2 style={{ margin: '0 0 6px', fontSize: 20, fontWeight: 700, color: 'var(--text)' }}>{title}</h2>
{description && <p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 14 }}>{description}</p>}
</div>
);
}
const DEFAULT = {
appName: 'Crowdlending Tracker',
appUrl: '',
allowRegistration: true,
minPasswordLength: 8,
};
export default function GeneralSection() {
const [form, setForm] = useState(DEFAULT);
const [saved, setSaved] = useState(null); // 'ok' | 'err' | null
const [busy, setBusy] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get('/admin/general')
.then(d => setForm({
appName: d.appName || 'Crowdlending Tracker',
appUrl: d.appUrl || '',
allowRegistration: d.allowRegistration !== false,
minPasswordLength: d.minPasswordLength || 8,
}))
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
const save = async () => {
setBusy(true); setSaved(null);
try {
await api.patch('/admin/general', {
appName: form.appName.trim(),
appUrl: form.appUrl.trim(),
allowRegistration: form.allowRegistration,
minPasswordLength: form.minPasswordLength,
});
setSaved('ok');
setTimeout(() => setSaved(null), 3000);
} catch {
setSaved('err');
} finally {
setBusy(false);
}
};
if (loading) return <div style={{ padding: 32, color: 'var(--text-muted)' }}>Chargement</div>;
return (
<div>
<SectionHeader
title="Paramètres généraux"
description="Identité de l'application et règles d'accès."
/>
{/* Identité */}
<div className="card" style={{ marginBottom: 24 }}>
<h3 style={{ margin: '0 0 4px', fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Identité</h3>
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--text-muted)' }}>
Nom et URL affichés dans les emails et sur la page de connexion.
</p>
<SettingRow label="Nom de la plateforme" description="Affiché dans l'en-tête et le pied de page des emails envoyés.">
<input
className="form-input"
type="text"
maxLength={100}
value={form.appName}
onChange={e => set('appName', e.target.value)}
style={{ width: '100%' }}
/>
</SettingRow>
<SettingRow label="URL de la plateforme" description="Utilisée pour les boutons de redirection dans les emails. Inclure le protocole (https://).">
<input
className="form-input"
type="url"
maxLength={500}
value={form.appUrl}
placeholder="https://mon-app.example.com"
onChange={e => set('appUrl', e.target.value)}
style={{ width: '100%' }}
/>
</SettingRow>
</div>
{/* Accès */}
<div className="card" style={{ marginBottom: 24 }}>
<h3 style={{ margin: '0 0 4px', fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Accès</h3>
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--text-muted)' }}>
Contrôle qui peut créer un compte sur l'application.
</p>
<SettingRow
label="Auto-inscription"
description="Permettre aux visiteurs de créer un compte sans invitation. Si désactivé, seules les invitations permettent de s'inscrire."
>
<label style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer' }}>
<div
role="switch"
aria-checked={form.allowRegistration}
onClick={() => set('allowRegistration', !form.allowRegistration)}
style={{
width: 40, height: 22, borderRadius: 11,
background: form.allowRegistration ? 'var(--primary, #2563eb)' : 'var(--border)',
position: 'relative', cursor: 'pointer',
transition: 'background .2s',
flexShrink: 0,
}}
>
<div style={{
position: 'absolute', top: 3, left: form.allowRegistration ? 21 : 3,
width: 16, height: 16, borderRadius: '50%', background: '#fff',
transition: 'left .2s',
boxShadow: '0 1px 3px rgba(0,0,0,.2)',
}} />
</div>
<span style={{ fontSize: 13, color: 'var(--text)' }}>
{form.allowRegistration ? 'Activée' : 'Désactivée — invitation uniquement'}
</span>
</label>
</SettingRow>
</div>
{/* Sécurité */}
<div className="card" style={{ marginBottom: 28 }}>
<h3 style={{ margin: '0 0 4px', fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Sécurité</h3>
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--text-muted)' }}>
Règles appliquées à la création et la modification des mots de passe.
</p>
<SettingRow
label="Longueur minimale du mot de passe"
description="Nombre de caractères minimum requis. Appliqué sur tous les écrans de création et changement de mot de passe."
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<input
className="form-input"
type="number"
min={6}
max={64}
value={form.minPasswordLength}
onChange={e => set('minPasswordLength', Math.max(6, Math.min(64, parseInt(e.target.value) || 8)))}
style={{ width: 80 }}
/>
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>caractères (min 6, max 64)</span>
</div>
</SettingRow>
</div>
{/* Bouton sauvegarde */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<button
onClick={save}
disabled={busy}
className="btn-primary"
style={{ padding: '9px 22px' }}
>
{busy ? 'Enregistrement…' : 'Enregistrer'}
</button>
{saved === 'ok' && (
<span style={{ fontSize: 13, color: 'var(--success, #16a34a)', display: 'flex', alignItems: 'center', gap: 6 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
Paramètres enregistrés
</span>
)}
{saved === 'err' && (
<span style={{ fontSize: 13, color: 'var(--danger, #dc2626)' }}>Erreur lors de la sauvegarde.</span>
)}
</div>
</div>
);
}
-31
View File
@@ -77,8 +77,6 @@ const DEFAULT_STATE = {
username: '',
password: '',
allowUnauth: false,
appName: 'Crowdlending Tracker',
appUrl: '',
};
export default function SmtpSection() {
@@ -109,8 +107,6 @@ export default function SmtpSection() {
username: data.username || '',
password: '',
allowUnauth: !!data.allowUnauth,
appName: data.appName || 'Crowdlending Tracker',
appUrl: data.appUrl || '',
});
setHasPassword(!!data.hasPassword);
hasPasswordRef.current = !!data.hasPassword;
@@ -239,33 +235,6 @@ export default function SmtpSection() {
<div className="card" style={{ padding: '0 24px' }}>
{/* Nom de la plateforme */}
<SettingRow label="Nom de la plateforme" description="Affiché dans l'en-tête et le pied de page des emails envoyés.">
<input
className="form-input"
type="text"
value={form.appName}
onChange={e => set('appName', e.target.value)}
placeholder="Crowdlending"
style={{ width: 280 }}
/>
</SettingRow>
{/* URL de la plateforme */}
<SettingRow label="URL de la plateforme" description="Utilisée pour le bouton de redirection dans les emails. Inclure le protocole (https://).">
<input
className="form-input"
type="url"
value={form.appUrl}
onChange={e => set('appUrl', e.target.value)}
placeholder="https://monapp.example.com"
style={{ width: 280 }}
/>
</SettingRow>
{/* Séparateur */}
<div style={{ borderBottom: '1px solid var(--border)', margin: '4px 0' }} />
{/* Activer */}
<SettingRow
label="Activer"
+9 -2
View File
@@ -308,6 +308,13 @@ function ResendInviteModal({ user, onClose, onSuccess, onError }) {
// ── Modale création utilisateur ────────────────────────────────────────────
function CreateUserModal({ open, onClose, onCreated }) {
const [minPasswordLength, setMinPasswordLength] = useState(8);
useEffect(() => {
fetch('/api/app-info').then(r => r.json()).then(d => {
if (d.minPasswordLength) setMinPasswordLength(d.minPasswordLength);
}).catch(() => {});
}, []);
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
const [loading, setLoading] = useState(false);
const [err, setErr] = useState(null);
@@ -356,8 +363,8 @@ function CreateUserModal({ open, onClose, onCreated }) {
</div>
<div>
<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" />
<PasswordStrength password={form.password} />
<input type="password" required minLength={minPasswordLength} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder={`${minPasswordLength} caractères minimum`} />
<PasswordStrength password={form.password} minLength={minPasswordLength} />
</div>
<div>
<label>Rôle</label>