feat(admin): add email change flow with confirmation for users
- add `ConfirmEmailChangePage.client.js` for email change token confirmation - add `emailChange.js` core utility to generate and verify email change tokens - add `EmailChangeConfirmEmail.js` and `EmailChangeNotifyEmail.js` email templates - update `UserEditModal` to handle email changes with password verification for self-edits - update `ProfilePage` to support email change initiation - update `UsersPage` to pass `currentUserId` to `UserEditModal` - add email change API endpoints in `auth/api.js` and `auth/email.js` - register `ConfirmEmailChangePage` in `AdminPage.client.js`
This commit is contained in:
@@ -20,12 +20,48 @@ const ProfilePage = ({ user: initialUser }) => {
|
||||
const [uploadingImage, setUploadingImage] = useState(false);
|
||||
const [formData, setFormData] = useState({ name: initialUser?.name || '' });
|
||||
|
||||
const [emailFormOpen, setEmailFormOpen] = useState(false);
|
||||
const [emailFormData, setEmailFormData] = useState({ newEmail: '', password: '' });
|
||||
const [emailLoading, setEmailLoading] = useState(false);
|
||||
const [pendingEmailMessage, setPendingEmailMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialUser) setFormData({ name: initialUser.name || '' });
|
||||
}, [initialUser]);
|
||||
|
||||
const hasChanges = formData.name !== user?.name;
|
||||
|
||||
const handleEmailSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!emailFormData.newEmail.trim()) {
|
||||
toast.error('Le nouveau courriel est requis');
|
||||
return;
|
||||
}
|
||||
if (!emailFormData.password) {
|
||||
toast.error('Le mot de passe est requis');
|
||||
return;
|
||||
}
|
||||
setEmailLoading(true);
|
||||
try {
|
||||
const response = await fetch('/zen/api/users/profile/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ newEmail: emailFormData.newEmail.trim(), password: emailFormData.password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) throw new Error(data.error || data.message || 'Échec de la demande de changement de courriel');
|
||||
toast.success(data.message);
|
||||
setPendingEmailMessage(data.message);
|
||||
setEmailFormOpen(false);
|
||||
setEmailFormData({ newEmail: '', password: '' });
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Échec de la demande de changement de courriel');
|
||||
} finally {
|
||||
setEmailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name.trim()) {
|
||||
@@ -145,14 +181,56 @@ const ProfilePage = ({ user: initialUser }) => {
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
<Input
|
||||
label="Courriel"
|
||||
type="email"
|
||||
value={user?.email || ''}
|
||||
disabled
|
||||
readOnly
|
||||
description="L'email ne peut pas être modifié"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Input
|
||||
label="Courriel"
|
||||
type="email"
|
||||
value={user?.email || ''}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
{pendingEmailMessage ? (
|
||||
<p className="text-xs text-blue-600 dark:text-blue-400">{pendingEmailMessage}</p>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEmailFormOpen(prev => !prev); setPendingEmailMessage(''); setEmailFormData({ newEmail: '', password: '' }); }}
|
||||
className="text-xs text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-200 underline self-start transition-colors"
|
||||
>
|
||||
{emailFormOpen ? 'Annuler' : 'Modifier le courriel'}
|
||||
</button>
|
||||
)}
|
||||
{emailFormOpen && (
|
||||
<form onSubmit={handleEmailSubmit} className="flex flex-col gap-3 mt-2 p-3 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
|
||||
<Input
|
||||
label="Nouveau courriel"
|
||||
type="email"
|
||||
value={emailFormData.newEmail}
|
||||
onChange={(value) => setEmailFormData(prev => ({ ...prev, newEmail: value }))}
|
||||
placeholder="nouvelle@adresse.com"
|
||||
required
|
||||
disabled={emailLoading}
|
||||
/>
|
||||
<Input
|
||||
label="Mot de passe actuel"
|
||||
type="password"
|
||||
value={emailFormData.password}
|
||||
onChange={(value) => setEmailFormData(prev => ({ ...prev, password: value }))}
|
||||
placeholder="Votre mot de passe"
|
||||
required
|
||||
disabled={emailLoading}
|
||||
/>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button type="button" variant="secondary" onClick={() => { setEmailFormOpen(false); setEmailFormData({ newEmail: '', password: '' }); }} disabled={emailLoading}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" loading={emailLoading} disabled={emailLoading}>
|
||||
Envoyer la confirmation
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
label="Compte créé"
|
||||
type="text"
|
||||
|
||||
Reference in New Issue
Block a user