Files
crowdlending-app/frontend/src/pages/admin/SmtpSection.jsx
T
2026-06-15 23:03:37 +02:00

386 lines
15 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef, useCallback } from 'react';
import { api } from '../../api.js';
import { useAuth } from '../../context/AuthContext.jsx';
/* ── Icône œil (show/hide password) ───────────────────────────────────── */
function IconEye({ open }) {
return open
? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>;
}
/* ── Toggle switch ─────────────────────────────────────────────────────── */
function Toggle({ checked, onChange, id }) {
return (
<label htmlFor={id} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
<input
id={id}
type="checkbox"
checked={checked}
onChange={e => onChange(e.target.checked)}
style={{ display: 'none' }}
/>
<span style={{
display: 'inline-block',
width: 44,
height: 24,
borderRadius: 12,
background: checked ? 'var(--primary)' : 'var(--border)',
position: 'relative',
transition: 'background 0.2s',
flexShrink: 0,
}}>
<span style={{
position: 'absolute',
top: 3,
left: checked ? 23 : 3,
width: 18,
height: 18,
borderRadius: '50%',
background: '#fff',
transition: 'left 0.2s',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
}} />
</span>
</label>
);
}
/* ── Ligne de paramètre ────────────────────────────────────────────────── */
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>
);
}
const DEFAULT_STATE = {
enabled: false,
host: '',
port: 587,
secure: false,
email: '',
username: '',
password: '',
allowUnauth: false,
};
export default function SmtpSection() {
const { user } = useAuth();
const [form, setForm] = useState(DEFAULT_STATE);
const [hasPassword, setHasPassword] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const passwordTyped = useRef(false); // true uniquement si l'utilisateur a tapé manuellement
const [testEmail, setTestEmail] = useState(user?.email || '');
const [result, setResult] = useState(null); // { ok, msg }
const [testResult, setTestResult] = useState(null);
const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false);
const [loadingEnv, setLoadingEnv] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'saving' | 'saved' | 'error'
const debounceRef = useRef(null);
const isFirstLoad = useRef(true);
const hasPasswordRef = useRef(false);
useEffect(() => {
api.get('/admin/smtp').then(data => {
setForm({
enabled: !!data.enabled,
host: data.host || '',
port: data.port || 587,
secure: !!data.secure,
email: data.email || '',
username: data.username || '',
password: '',
allowUnauth: !!data.allowUnauth,
});
setHasPassword(!!data.hasPassword);
hasPasswordRef.current = !!data.hasPassword;
});
}, []);
const doSave = useCallback(async (currentForm) => {
setSaveStatus('saving');
try {
const payload = { ...currentForm };
// N'envoyer le mot de passe que si l'utilisateur l'a tapé manuellement
if (!payload.password || !passwordTyped.current) delete payload.password;
await api.put('/admin/smtp', payload);
if (payload.password) {
setHasPassword(true);
hasPasswordRef.current = true;
}
setSaveStatus('saved');
setTimeout(() => setSaveStatus(null), 2500);
} catch (e) {
setSaveStatus('error');
setResult({ ok: false, msg: e.message || 'Erreur lors de la sauvegarde.' });
}
}, []);
const set = (key, value) => {
setForm(f => {
const next = { ...f, [key]: value };
// Ne pas déclencher l'auto-save au chargement initial
if (isFirstLoad.current) { isFirstLoad.current = false; return next; }
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => doSave(next), 1500);
return next;
});
};
// Nettoyage du timer au démontage
useEffect(() => () => clearTimeout(debounceRef.current), []);
const handleTest = async () => {
if (!testEmail) return;
setTesting(true);
setTestResult(null);
try {
const data = await api.post('/admin/smtp/test', { to: testEmail });
setTestResult({ ok: data.ok, msg: data.msg });
} catch (e) {
setTestResult({ ok: false, msg: e.message || 'Erreur lors du test.' });
} finally {
setTesting(false);
}
};
const handleLoadEnv = async () => {
setLoadingEnv(true);
try {
const data = await api.get('/admin/smtp/env');
setForm(f => {
const next = {
...f,
host: data.host || f.host,
port: data.port || f.port,
email: data.email || f.email,
username: data.username || f.username,
password: '',
};
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => doSave(next), 1500);
return next;
});
if (data.hasPassword) { setHasPassword(true); hasPasswordRef.current = true; }
} catch (e) {
setResult({ ok: false, msg: 'Impossible de charger les valeurs .env.' });
} finally {
setLoadingEnv(false);
}
};
return (
<div className="account-section">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<h2 style={{ margin: 0 }}>SMTP</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{saveStatus === 'saving' && (
<span style={{ fontSize: '0.82rem', color: 'var(--text-muted)' }}>Sauvegarde</span>
)}
{saveStatus === 'saved' && (
<span style={{ fontSize: '0.82rem', color: 'var(--success, #15803d)' }}> Sauvegardé</span>
)}
{saveStatus === 'error' && (
<span style={{ fontSize: '0.82rem', color: 'var(--danger, #dc2626)' }}>Erreur</span>
)}
<button
className="btn btn-secondary"
onClick={() => handleLoadEnv()}
disabled={loadingEnv}
style={{ fontSize: '0.82rem' }}
>
{loadingEnv ? 'Chargement…' : 'Charger depuis .env'}
</button>
</div>
</div>
<p style={{ color: 'var(--text-muted)', marginBottom: 24 }}>
Configuration du serveur de messagerie sortant. Utilisée pour les alertes, rapports et réinitialisation de mot de passe.
</p>
{result && (
<div
className={result.ok ? 'alert-success' : 'alert-error'}
style={{
padding: '10px 14px',
borderRadius: 6,
marginBottom: 16,
background: result.ok ? 'var(--success-bg, #ecfdf5)' : 'var(--danger-bg, #fef2f2)',
color: result.ok ? 'var(--success, #15803d)' : 'var(--danger, #dc2626)',
border: `1px solid ${result.ok ? 'var(--success, #86efac)' : 'var(--danger, #fca5a5)'}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>{result.msg}</span>
<button onClick={() => setResult(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'inherit', fontSize: 16 }}>×</button>
</div>
)}
<div className="card" style={{ padding: '0 24px' }}>
{/* Activer */}
<SettingRow
label="Activer"
description="Active SMTP. Activez ceci uniquement si vous avez saisi l'hôte, le port, le courriel, l'utilisateur et son mot de passe."
>
<Toggle id="smtp-enabled" checked={form.enabled} onChange={v => set('enabled', v)} />
</SettingRow>
{/* Hôte */}
<SettingRow label="Hôte" description="Nom du serveur SMTP">
<input
className="form-input"
type="text"
value={form.host}
onChange={e => set('host', e.target.value)}
placeholder="smtp.example.com"
style={{ width: 280 }}
/>
</SettingRow>
{/* Port */}
<SettingRow label="Port" description="Port du serveur SMTP">
<input
className="form-input"
type="number"
value={form.port}
onChange={e => set('port', parseInt(e.target.value, 10) || 587)}
min={1}
max={65535}
style={{ width: 120 }}
/>
</SettingRow>
{/* Connexion sécurisée (TLS) */}
<SettingRow label="Connexion sécurisée (TLS)" description="Utiliser TLS dès la connexion (port 465). Désactivez pour STARTTLS (port 587).">
<Toggle id="smtp-secure" checked={form.secure} onChange={v => set('secure', v)} />
</SettingRow>
{/* Courriel */}
<SettingRow label="Courriel" description="Adresse email à partir de laquelle les courriels sont envoyés">
<input
className="form-input"
type="email"
value={form.email}
onChange={e => set('email', e.target.value)}
placeholder="no-reply@example.com"
style={{ width: 280 }}
/>
</SettingRow>
{/* Nom d'utilisateur */}
<SettingRow label="Nom d'utilisateur" description="Nom d'utilisateur du serveur SMTP">
<input
className="form-input"
type="text"
value={form.username}
onChange={e => set('username', e.target.value)}
placeholder="user@example.com"
autoComplete="off"
style={{ width: 280 }}
/>
</SettingRow>
{/* Mot de passe */}
<SettingRow
label="Mot de passe"
description={hasPassword && !form.password ? 'Un mot de passe est déjà enregistré. Laissez vide pour le conserver.' : 'Mot de passe du serveur SMTP'}
>
<div style={{ position: 'relative', width: 280 }}>
<input
className="form-input"
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={e => { passwordTyped.current = true; set('password', e.target.value); }}
placeholder={hasPassword ? '••••••••' : 'Mot de passe'}
autoComplete="new-password"
style={{ width: '100%', paddingRight: 36 }}
/>
<button
type="button"
onClick={() => setShowPassword(s => !s)}
style={{
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--text-muted)', padding: 0, display: 'flex',
}}
tabIndex={-1}
>
<IconEye open={showPassword} />
</button>
</div>
</SettingRow>
{/* Certificats non autorisés */}
<SettingRow
label="Faire confiance aux certificats de serveurs SMTP non autorisés"
description="Ne permettez ceci que si vous avez besoin de faire confiance aux certificats autosignés."
>
<Toggle id="smtp-allowUnauth" checked={form.allowUnauth} onChange={v => set('allowUnauth', v)} />
</SettingRow>
</div>
{/* Section email de test */}
<div className="card" style={{ marginTop: 24, padding: '20px 24px' }}>
<h3 style={{ margin: '0 0 12px' }}>Envoyer un email de test</h3>
<p style={{ color: 'var(--text-muted)', marginBottom: 16, fontSize: '0.9rem' }}>
Vérifiez que votre configuration SMTP fonctionne en envoyant un email de test. Sauvegardez d'abord vos paramètres.
</p>
{testResult && (
<div style={{
padding: '10px 14px',
borderRadius: 6,
marginBottom: 14,
background: testResult.ok ? 'var(--success-bg, #ecfdf5)' : 'var(--danger-bg, #fef2f2)',
color: testResult.ok ? 'var(--success, #15803d)' : 'var(--danger, #dc2626)',
border: `1px solid ${testResult.ok ? 'var(--success, #86efac)' : 'var(--danger, #fca5a5)'}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<span>{testResult.msg}</span>
<button onClick={() => setTestResult(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'inherit', fontSize: 16 }}>×</button>
</div>
)}
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<input
className="form-input"
type="email"
value={testEmail}
onChange={e => setTestEmail(e.target.value)}
placeholder="destinataire@example.com"
style={{ flex: 1, maxWidth: 320 }}
onKeyDown={e => e.key === 'Enter' && handleTest()}
/>
<button
className="btn btn-secondary"
onClick={() => handleTest()}
disabled={testing || !testEmail}
>
{testing ? 'Envoi' : 'Envoyer un courriel de test'}
</button>
</div>
</div>
</div>
);
}