11import { 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'
33import type { User as UserType , UpdateUserRolesRequest } from '@/types/users'
44import toast from 'react-hot-toast'
5+ import { apiClient } from '@/services/api'
56
67interface 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