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:
@@ -4,14 +4,15 @@ import { useState, useEffect } from 'react';
|
||||
import { Input, Select, TagInput, Modal } from '@zen/core/shared/components';
|
||||
import { useToast } from '@zen/core/toast';
|
||||
|
||||
const UserEditModal = ({ userId, isOpen, onClose, onSaved }) => {
|
||||
const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
|
||||
const toast = useToast();
|
||||
const isSelf = userId && currentUserId && userId === currentUserId;
|
||||
|
||||
const [userData, setUserData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({ name: '', email_verified: 'false' });
|
||||
const [formData, setFormData] = useState({ name: '', email: '', email_verified: 'false', currentPassword: '' });
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const [allRoles, setAllRoles] = useState([]);
|
||||
@@ -47,7 +48,9 @@ const UserEditModal = ({ userId, isOpen, onClose, onSaved }) => {
|
||||
setUserData(userJson.user);
|
||||
setFormData({
|
||||
name: userJson.user.name || '',
|
||||
email: userJson.user.email || '',
|
||||
email_verified: userJson.user.email_verified ? 'true' : 'false',
|
||||
currentPassword: '',
|
||||
});
|
||||
} else {
|
||||
toast.error(userJson.message || 'Utilisateur introuvable');
|
||||
@@ -73,9 +76,12 @@ const UserEditModal = ({ userId, isOpen, onClose, onSaved }) => {
|
||||
if (errors[field]) setErrors(prev => ({ ...prev, [field]: null }));
|
||||
};
|
||||
|
||||
const emailChanged = userData && formData.email !== userData.email;
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
if (!formData.name?.trim()) newErrors.name = 'Le nom est requis';
|
||||
if (emailChanged && isSelf && !formData.currentPassword) newErrors.currentPassword = 'Le mot de passe est requis pour changer le courriel';
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
@@ -120,7 +126,43 @@ const UserEditModal = ({ userId, isOpen, onClose, onSaved }) => {
|
||||
),
|
||||
]);
|
||||
|
||||
toast.success('Utilisateur mis à jour');
|
||||
if (emailChanged) {
|
||||
if (isSelf) {
|
||||
const emailRes = await fetch('/zen/api/users/profile/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ newEmail: formData.email.trim(), password: formData.currentPassword }),
|
||||
});
|
||||
const emailData = await emailRes.json();
|
||||
if (!emailRes.ok) {
|
||||
toast.error(emailData.error || emailData.message || 'Impossible de changer le courriel');
|
||||
onSaved?.();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
toast.success('Utilisateur mis à jour');
|
||||
toast.info(emailData.message || 'Un courriel de confirmation a été envoyé');
|
||||
} else {
|
||||
const emailRes = await fetch(`/zen/api/users/${userId}/email`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ newEmail: formData.email.trim() }),
|
||||
});
|
||||
const emailData = await emailRes.json();
|
||||
if (!emailRes.ok) {
|
||||
toast.error(emailData.error || emailData.message || 'Impossible de changer le courriel');
|
||||
onSaved?.();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
toast.success('Utilisateur mis à jour');
|
||||
}
|
||||
} else {
|
||||
toast.success('Utilisateur mis à jour');
|
||||
}
|
||||
|
||||
onSaved?.();
|
||||
onClose();
|
||||
} catch {
|
||||
@@ -165,11 +207,24 @@ const UserEditModal = ({ userId, isOpen, onClose, onSaved }) => {
|
||||
error={errors.name}
|
||||
/>
|
||||
<Input
|
||||
label="Email"
|
||||
value={userData?.email || ''}
|
||||
disabled
|
||||
label="Courriel"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(value) => handleInputChange('email', value)}
|
||||
placeholder="courriel@exemple.com"
|
||||
/>
|
||||
</div>
|
||||
{isSelf && emailChanged && (
|
||||
<Input
|
||||
label="Mot de passe actuel *"
|
||||
type="password"
|
||||
value={formData.currentPassword}
|
||||
onChange={(value) => handleInputChange('currentPassword', value)}
|
||||
placeholder="Votre mot de passe"
|
||||
error={errors.currentPassword}
|
||||
description="Requis pour confirmer le changement de courriel"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label="Email vérifié"
|
||||
|
||||
Reference in New Issue
Block a user