From 484f2f3a62cded8e3748cb9ef985b18d3b9bdb95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:06:16 +0000 Subject: [PATCH] Add Confirm Password field to My Profile password change form Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/frontend/web/public/locales/en.json | 3 + .../auth/components/UserManagement.tsx | 151 +++++++----- .../features/auth/components/UserProfile.tsx | 229 ++++++++++++------ 3 files changed, 259 insertions(+), 124 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d520c6416cc..3dc9ebe98e5 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -64,6 +64,9 @@ "currentPasswordPlaceholder": "Current password", "newPassword": "New Password", "newPasswordPlaceholder": "New password", + "confirmPassword": "Confirm New Password", + "confirmPasswordPlaceholder": "Confirm new password", + "passwordsDoNotMatch": "Passwords do not match", "saveSuccess": "Profile updated successfully", "saveFailed": "Failed to save profile. Please try again." }, diff --git a/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx b/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx index 48dbf211105..4dd88ca1e5a 100644 --- a/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx +++ b/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx @@ -8,6 +8,8 @@ import { FormControl, FormErrorMessage, FormLabel, + Grid, + GridItem, Heading, IconButton, Input, @@ -38,7 +40,15 @@ import { selectCurrentUser } from 'features/auth/store/authSlice'; import type { ChangeEvent, FormEvent } from 'react'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiArrowLeftBold, PiEyeBold, PiEyeSlashBold, PiPencilBold, PiPlusBold, PiTrashBold } from 'react-icons/pi'; +import { + PiArrowLeftBold, + PiEyeBold, + PiEyeSlashBold, + PiLightningFill, + PiPencilBold, + PiPlusBold, + PiTrashBold, +} from 'react-icons/pi'; import { useNavigate } from 'react-router-dom'; import type { UserDTO } from 'services/api/endpoints/auth'; import { @@ -68,6 +78,8 @@ const validatePasswordStrength = ( return { isValid: true, message: '' }; }; +const FORM_GRID_COLUMNS = '120px 1fr'; + // --------------------------------------------------------------------------- // Create / Edit user modal // --------------------------------------------------------------------------- @@ -206,71 +218,100 @@ const UserFormModal = memo(({ isOpen, onClose, editUser }: UserFormModalProps) = {!isEdit && ( - {t('auth.userManagement.email')} - + + + + {t('auth.userManagement.email')} + + + + + + )} - {t('auth.userManagement.displayName')} - + + + + {t('auth.userManagement.displayName')} + + + + + + 0 && !passwordValidation.isValid} isRequired={!isEdit}> - - {isEdit ? t('auth.userManagement.newPassword') : t('auth.userManagement.password')} - - - - - - + + + {isEdit ? t('auth.userManagement.newPassword') : t('auth.userManagement.password')} + + + + + : } - variant="ghost" - size="sm" - onClick={toggleShowPassword} - tabIndex={-1} + autoComplete="new-password" + pr="4.5rem" /> - - - - {password.length > 0 && !passwordValidation.isValid && ( - {passwordValidation.message} - )} + + + : } + variant="ghost" + size="sm" + onClick={toggleShowPassword} + tabIndex={-1} + /> + + + + {password.length > 0 && !passwordValidation.isValid && ( + {passwordValidation.message} + )} + + - + + + + + + {t('auth.userManagement.isAdmin')} diff --git a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx index 32271924227..4504698f0ea 100644 --- a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx +++ b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx @@ -7,6 +7,8 @@ import { FormErrorMessage, FormHelperText, FormLabel, + Grid, + GridItem, Heading, IconButton, Input, @@ -22,7 +24,7 @@ import { selectAuthToken, selectCurrentUser, setCredentials } from 'features/aut import type { ChangeEvent, FormEvent } from 'react'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiEyeBold, PiEyeSlashBold } from 'react-icons/pi'; +import { PiEyeBold, PiEyeSlashBold, PiLightningFill } from 'react-icons/pi'; import { useNavigate } from 'react-router-dom'; import { useLazyGeneratePasswordQuery, useUpdateCurrentUserMutation } from 'services/api/endpoints/auth'; @@ -45,6 +47,8 @@ const validatePasswordStrength = ( return { isValid: true, message: '' }; }; +const PASSWORD_GRID_COLUMNS = '180px 1fr'; + export const UserProfile = memo(() => { const { t } = useTranslation(); const currentUser = useAppSelector(selectCurrentUser); @@ -55,8 +59,10 @@ export const UserProfile = memo(() => { const [displayName, setDisplayName] = useState(currentUser?.display_name ?? ''); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [updateCurrentUser, { isLoading }] = useUpdateCurrentUserMutation(); @@ -65,8 +71,9 @@ export const UserProfile = memo(() => { const newPasswordValidation = validatePasswordStrength(newPassword, t); const isPasswordChangeAttempted = newPassword.length > 0 || currentPassword.length > 0; + const passwordsMatch = newPassword.length > 0 && newPassword === confirmPassword; const isPasswordChangeValid = - !isPasswordChangeAttempted || (currentPassword.length > 0 && newPasswordValidation.isValid); + !isPasswordChangeAttempted || (currentPassword.length > 0 && newPasswordValidation.isValid && passwordsMatch); const handleCancel = useCallback(() => { navigate(-1); @@ -76,7 +83,9 @@ export const UserProfile = memo(() => { try { const result = await triggerGeneratePassword().unwrap(); setNewPassword(result.password); + setConfirmPassword(result.password); setShowNewPassword(true); + setShowConfirmPassword(true); } catch { // ignore } @@ -90,6 +99,10 @@ export const UserProfile = memo(() => { setShowNewPassword((v) => !v); }, []); + const toggleShowConfirmPassword = useCallback(() => { + setShowConfirmPassword((v) => !v); + }, []); + const handleDisplayNameChange = useCallback((e: ChangeEvent) => { setDisplayName(e.target.value); }, []); @@ -102,6 +115,10 @@ export const UserProfile = memo(() => { setNewPassword(e.target.value); }, []); + const handleConfirmPasswordChange = useCallback((e: ChangeEvent) => { + setConfirmPassword(e.target.value); + }, []); + const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); @@ -201,80 +218,154 @@ export const UserProfile = memo(() => { {/* Current password */} 0}> - {t('auth.profile.currentPassword')} - - - - - : } - variant="ghost" - size="sm" - onClick={toggleShowCurrentPassword} - tabIndex={-1} + + + + {t('auth.profile.currentPassword')} + + + + + - - - + + + : } + variant="ghost" + size="sm" + onClick={toggleShowCurrentPassword} + tabIndex={-1} + /> + + + + + {/* New password */} - 0 && !newPasswordValidation.isValid} mb={2}> - {t('auth.profile.newPassword')} - - - - - : } - variant="ghost" - size="sm" - onClick={toggleShowNewPassword} - tabIndex={-1} + 0 && !newPasswordValidation.isValid} mb={4}> + + + + {t('auth.profile.newPassword')} + + + + + - - - - {newPassword.length > 0 && !newPasswordValidation.isValid && ( - {newPasswordValidation.message} - )} + + + : } + variant="ghost" + size="sm" + onClick={toggleShowNewPassword} + tabIndex={-1} + /> + + + + {newPassword.length > 0 && !newPasswordValidation.isValid && ( + {newPasswordValidation.message} + )} + + - + {/* Confirm new password */} + 0 && !passwordsMatch} mb={4}> + + + + {t('auth.profile.confirmPassword')} + + + + + + + + : } + variant="ghost" + size="sm" + onClick={toggleShowConfirmPassword} + tabIndex={-1} + /> + + + + {confirmPassword.length > 0 && !passwordsMatch && ( + {t('auth.profile.passwordsDoNotMatch')} + )} + + + + + {/* Generate password button – aligned with the input column */} + + + + + + {errorMessage && (