feat: ajout changement de mot de passe + bind mounts persistants + credentials en variables d'env
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Page Paramètres — 4 sections :
|
||||
* 1. SMTP (config email par utilisateur)
|
||||
* 2. Sociétés (nom + email de remboursement)
|
||||
* 3. Catégories
|
||||
* 4. Microsoft 365 / SharePoint (Azure App Registration + fichier Excel commun)
|
||||
* Page Paramètres — 5 sections :
|
||||
* 1. Mot de passe
|
||||
* 2. SMTP (config email par utilisateur)
|
||||
* 3. Sociétés (nom + email de remboursement)
|
||||
* 4. Catégories
|
||||
* 5. Microsoft 365 / SharePoint (Azure App Registration + fichier Excel commun)
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -12,6 +13,109 @@ import { useRef } from 'react';
|
||||
import api from '../api/client';
|
||||
import type { Company, Category, SmtpConfig, AppSettings, Contact } from '../types';
|
||||
|
||||
// ─── Section Mot de passe ────────────────────────────────────
|
||||
|
||||
function PasswordSection() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form, setForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const change = useMutation({
|
||||
mutationFn: (payload: typeof form) => api.patch('/auth/password', payload),
|
||||
onSuccess: () => {
|
||||
toast.success('Mot de passe modifié');
|
||||
setForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
setErrors({});
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (e: any) => {
|
||||
const msg = e.response?.data?.error ?? 'Erreur';
|
||||
toast.error(msg);
|
||||
},
|
||||
});
|
||||
|
||||
function validate() {
|
||||
const errs: Record<string, string> = {};
|
||||
if (!form.currentPassword) errs.currentPassword = 'Requis';
|
||||
if (form.newPassword.length < 8) errs.newPassword = 'Au moins 8 caractères';
|
||||
if (form.newPassword !== form.confirmPassword) errs.confirmPassword = 'Les mots de passe ne correspondent pas';
|
||||
setErrors(errs);
|
||||
return Object.keys(errs).length === 0;
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (validate()) change.mutate(form);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<button onClick={() => setOpen(!open)}
|
||||
className="flex items-center justify-between w-full p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-rose-50 flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-rose-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-semibold text-sm text-gray-900">Mot de passe</p>
|
||||
<p className="text-xs text-gray-400">Modifier votre mot de passe de connexion</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg className={`w-5 h-5 text-gray-300 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<form onSubmit={handleSubmit} className="px-4 pb-4 space-y-3 border-t border-gray-50 pt-3">
|
||||
<div>
|
||||
<label className="form-label">Mot de passe actuel</label>
|
||||
<input
|
||||
className={`form-input ${errors.currentPassword ? 'border-red-400' : ''}`}
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={form.currentPassword}
|
||||
onChange={e => setForm(f => ({ ...f, currentPassword: e.target.value }))}
|
||||
/>
|
||||
{errors.currentPassword && <p className="text-xs text-red-500 mt-1">{errors.currentPassword}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Nouveau mot de passe</label>
|
||||
<input
|
||||
className={`form-input ${errors.newPassword ? 'border-red-400' : ''}`}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="8 caractères minimum"
|
||||
value={form.newPassword}
|
||||
onChange={e => setForm(f => ({ ...f, newPassword: e.target.value }))}
|
||||
/>
|
||||
{errors.newPassword && <p className="text-xs text-red-500 mt-1">{errors.newPassword}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label">Confirmer le nouveau mot de passe</label>
|
||||
<input
|
||||
className={`form-input ${errors.confirmPassword ? 'border-red-400' : ''}`}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={form.confirmPassword}
|
||||
onChange={e => setForm(f => ({ ...f, confirmPassword: e.target.value }))}
|
||||
/>
|
||||
{errors.confirmPassword && <p className="text-xs text-red-500 mt-1">{errors.confirmPassword}</p>}
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<button type="submit" disabled={change.isPending} className="btn-primary py-2 text-sm">
|
||||
{change.isPending ? 'Modification…' : 'Modifier le mot de passe'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Section SMTP ────────────────────────────────────────────
|
||||
|
||||
function SmtpSection() {
|
||||
@@ -564,6 +668,7 @@ export default function Settings() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-gray-900">Paramètres</h2>
|
||||
<PasswordSection />
|
||||
<SmtpSection />
|
||||
<CompaniesSection />
|
||||
<CategoriesSection />
|
||||
|
||||
Reference in New Issue
Block a user