Skip to content

Commit e622b4c

Browse files
committed
feat: add password change functionality to Edit User modal
Added collapsible password change section in the Edit User modal that allows users to change their password directly from the user management interface. Features: - Collapsible 'Change Password' section with toggle button - Current password validation - New password with confirmation field - Password visibility toggles (eye icons) for all fields - Frontend validation: - All fields required - Min 8 characters - Passwords must match - New password must differ from current - Clear password requirements hint - Success/error toast notifications - Integrates with existing PUT /api/users/{user_id}/password endpoint - Triggers automatic removal of default admin credentials (Admin123!) UX improvements: - Separate button for password change (doesn't interfere with role updates) - Fields reset when modal opens/closes - Loading states during password change - Clean, consistent styling with dark mode support This provides a convenient way for admins to change user passwords and for users to change their own password when editing their profile.
1 parent 406ffd8 commit e622b4c

1 file changed

Lines changed: 163 additions & 13 deletions

File tree

MakerMatrix/frontend/src/components/users/EditUserModal.tsx

Lines changed: 163 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState, useEffect } from 'react'
2-
import { X, User, Mail, Shield, Lock } from 'lucide-react'
2+
import { X, User, Mail, Shield, Lock, Eye, EyeOff, Key } from 'lucide-react'
33
import type { User as UserType, UpdateUserRolesRequest } from '@/types/users'
44
import toast from 'react-hot-toast'
5+
import { apiClient } from '@/services/api'
56

67
interface EditUserModalProps {
78
isOpen: boolean
@@ -20,10 +21,22 @@ const EditUserModal = ({
2021
}: EditUserModalProps) => {
2122
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([])
2223
const [loading, setLoading] = useState(false)
24+
const [showPasswordSection, setShowPasswordSection] = useState(false)
25+
const [currentPassword, setCurrentPassword] = useState('')
26+
const [newPassword, setNewPassword] = useState('')
27+
const [confirmPassword, setConfirmPassword] = useState('')
28+
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
29+
const [showNewPassword, setShowNewPassword] = useState(false)
30+
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
2331

2432
useEffect(() => {
2533
if (user) {
2634
setSelectedRoleIds(user.roles.map((r) => r.id))
35+
// Reset password fields when modal opens
36+
setShowPasswordSection(false)
37+
setCurrentPassword('')
38+
setNewPassword('')
39+
setConfirmPassword('')
2740
}
2841
}, [user])
2942

@@ -74,6 +87,53 @@ const EditUserModal = ({
7487
)
7588
}
7689

90+
const handlePasswordChange = async () => {
91+
if (!user) return
92+
93+
// Validation
94+
if (!currentPassword || !newPassword || !confirmPassword) {
95+
toast.error('Please fill in all password fields')
96+
return
97+
}
98+
99+
if (newPassword !== confirmPassword) {
100+
toast.error('New passwords do not match')
101+
return
102+
}
103+
104+
if (newPassword.length < 8) {
105+
toast.error('New password must be at least 8 characters')
106+
return
107+
}
108+
109+
if (newPassword === currentPassword) {
110+
toast.error('New password must be different from current password')
111+
return
112+
}
113+
114+
try {
115+
setLoading(true)
116+
const response = await apiClient.put(`/api/users/${user.id}/password`, {
117+
current_password: currentPassword,
118+
new_password: newPassword,
119+
})
120+
121+
if (response.status === 'success') {
122+
toast.success('Password changed successfully!')
123+
setShowPasswordSection(false)
124+
setCurrentPassword('')
125+
setNewPassword('')
126+
setConfirmPassword('')
127+
} else {
128+
toast.error(response.message || 'Failed to change password')
129+
}
130+
} catch (error: any) {
131+
toast.error(error?.response?.data?.detail || 'Failed to change password')
132+
} finally {
133+
setLoading(false)
134+
}
135+
}
136+
77137
if (!isOpen || !user) return null
78138

79139
return (
@@ -138,19 +198,109 @@ const EditUserModal = ({
138198
</div>
139199
</div>
140200

141-
{/* Note about password */}
142-
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
143-
<div className="flex gap-2">
144-
<Lock className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
145-
<div>
146-
<p className="text-xs text-blue-800 dark:text-blue-200 font-medium">
147-
Password Changes
148-
</p>
149-
<p className="text-xs text-blue-600 dark:text-blue-300 mt-1">
150-
To change password, use the "Reset Password" option in user settings.
151-
</p>
201+
{/* Password Change Section */}
202+
<div className="border border-border rounded-lg overflow-hidden">
203+
<button
204+
type="button"
205+
onClick={() => setShowPasswordSection(!showPasswordSection)}
206+
className="w-full flex items-center justify-between p-3 bg-background-secondary hover:bg-background-tertiary transition-colors"
207+
>
208+
<div className="flex items-center gap-2">
209+
<Key className="w-4 h-4 text-muted" />
210+
<span className="text-sm font-medium text-primary">Change Password</span>
152211
</div>
153-
</div>
212+
<Lock className={`w-4 h-4 text-muted transition-transform ${showPasswordSection ? 'rotate-90' : ''}`} />
213+
</button>
214+
215+
{showPasswordSection && (
216+
<div className="p-4 space-y-3 border-t border-border">
217+
{/* Current Password */}
218+
<div>
219+
<label className="block text-xs font-medium text-secondary mb-1">
220+
Current Password *
221+
</label>
222+
<div className="relative">
223+
<input
224+
type={showCurrentPassword ? 'text' : 'password'}
225+
value={currentPassword}
226+
onChange={(e) => setCurrentPassword(e.target.value)}
227+
className="input-field w-full pr-10"
228+
placeholder="Enter current password"
229+
/>
230+
<button
231+
type="button"
232+
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
233+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted hover:text-primary"
234+
>
235+
{showCurrentPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
236+
</button>
237+
</div>
238+
</div>
239+
240+
{/* New Password */}
241+
<div>
242+
<label className="block text-xs font-medium text-secondary mb-1">
243+
New Password *
244+
</label>
245+
<div className="relative">
246+
<input
247+
type={showNewPassword ? 'text' : 'password'}
248+
value={newPassword}
249+
onChange={(e) => setNewPassword(e.target.value)}
250+
className="input-field w-full pr-10"
251+
placeholder="Enter new password (min 8 chars)"
252+
/>
253+
<button
254+
type="button"
255+
onClick={() => setShowNewPassword(!showNewPassword)}
256+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted hover:text-primary"
257+
>
258+
{showNewPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
259+
</button>
260+
</div>
261+
</div>
262+
263+
{/* Confirm Password */}
264+
<div>
265+
<label className="block text-xs font-medium text-secondary mb-1">
266+
Confirm New Password *
267+
</label>
268+
<div className="relative">
269+
<input
270+
type={showConfirmPassword ? 'text' : 'password'}
271+
value={confirmPassword}
272+
onChange={(e) => setConfirmPassword(e.target.value)}
273+
className="input-field w-full pr-10"
274+
placeholder="Confirm new password"
275+
/>
276+
<button
277+
type="button"
278+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
279+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted hover:text-primary"
280+
>
281+
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
282+
</button>
283+
</div>
284+
</div>
285+
286+
{/* Password Requirements */}
287+
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded p-2">
288+
<p className="text-xs text-blue-800 dark:text-blue-200">
289+
Password must be at least 8 characters and different from current password.
290+
</p>
291+
</div>
292+
293+
{/* Change Password Button */}
294+
<button
295+
type="button"
296+
onClick={handlePasswordChange}
297+
disabled={loading}
298+
className="btn btn-primary w-full"
299+
>
300+
{loading ? 'Changing Password...' : 'Change Password'}
301+
</button>
302+
</div>
303+
)}
154304
</div>
155305

156306
{/* Actions */}

0 commit comments

Comments
 (0)