fix(admin): require current password for self password change and fix field ordering

- initialize `newPassword` in form state on load
- add `needsCurrentPassword` flag triggered by email or password change when editing self
- route self password change to profile endpoint with current password verification
- move role tag input above password section and update current password field visibility logic
This commit is contained in:
2026-04-24 15:52:34 -04:00
parent f22fcb6f68
commit ec0edf89b9
@@ -46,6 +46,7 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
name: userJson.user.name || '', name: userJson.user.name || '',
email: userJson.user.email || '', email: userJson.user.email || '',
currentPassword: '', currentPassword: '',
newPassword: '',
}); });
} else { } else {
toast.error(userJson.message || 'Utilisateur introuvable'); toast.error(userJson.message || 'Utilisateur introuvable');
@@ -90,10 +91,12 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
const emailChanged = userData && formData.email !== userData.email; const emailChanged = userData && formData.email !== userData.email;
const needsCurrentPassword = isSelf && (emailChanged || !!formData.newPassword);
const validate = () => { const validate = () => {
const newErrors = {}; const newErrors = {};
if (!formData.name?.trim()) newErrors.name = 'Le nom est requis'; 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'; if (needsCurrentPassword && !formData.currentPassword) newErrors.currentPassword = 'Le mot de passe actuel est requis';
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
}; };
@@ -175,11 +178,16 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
} }
if (formData.newPassword) { if (formData.newPassword) {
const pwdRes = await fetch(`/zen/api/users/${userId}/password`, { const pwdUrl = isSelf ? '/zen/api/users/profile/password' : `/zen/api/users/${userId}/password`;
method: 'PUT', const pwdMethod = isSelf ? 'POST' : 'PUT';
const pwdBody = isSelf
? { currentPassword: formData.currentPassword, newPassword: formData.newPassword }
: { newPassword: formData.newPassword };
const pwdRes = await fetch(pwdUrl, {
method: pwdMethod,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ newPassword: formData.newPassword }), body: JSON.stringify(pwdBody),
}); });
const pwdData = await pwdRes.json(); const pwdData = await pwdRes.json();
if (!pwdRes.ok) { if (!pwdRes.ok) {
@@ -242,17 +250,17 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
placeholder="courriel@exemple.com" placeholder="courriel@exemple.com"
/> />
</div> </div>
{isSelf && emailChanged && (
<Input <TagInput
label="Mot de passe actuel *" label="Rôles attribués"
type="password" options={roleOptions}
value={formData.currentPassword} value={selectedRoleIds}
onChange={(value) => handleInputChange('currentPassword', value)} onChange={setSelectedRoleIds}
placeholder="Votre mot de passe" placeholder="Rechercher un rôle..."
error={errors.currentPassword} renderTag={(opt, onRemove) => (
description="Requis pour confirmer le changement de courriel" <RoleBadge key={opt.value} name={opt.label} color={opt.color} onRemove={onRemove} />
/> )}
)} />
<div className="border-t border-neutral-200 dark:border-neutral-700 pt-4 flex flex-col gap-3"> <div className="border-t border-neutral-200 dark:border-neutral-700 pt-4 flex flex-col gap-3">
<Input <Input
@@ -275,16 +283,17 @@ const UserEditModal = ({ userId, currentUserId, isOpen, onClose, onSaved }) => {
</div> </div>
</div> </div>
<TagInput {needsCurrentPassword && (
label="Rôles attribués" <Input
options={roleOptions} label="Mot de passe actuel *"
value={selectedRoleIds} type="password"
onChange={setSelectedRoleIds} value={formData.currentPassword}
placeholder="Rechercher un rôle..." onChange={(value) => handleInputChange('currentPassword', value)}
renderTag={(opt, onRemove) => ( placeholder="Votre mot de passe"
<RoleBadge key={opt.value} name={opt.label} color={opt.color} onRemove={onRemove} /> error={errors.currentPassword}
)} description={emailChanged && formData.newPassword ? 'Requis pour confirmer le changement de courriel et de mot de passe' : emailChanged ? 'Requis pour confirmer le changement de courriel' : 'Requis pour confirmer le changement de mot de passe'}
/> />
)}
</div> </div>
)} )}
</Modal> </Modal>