Maj Section Général
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user