11import React , { useState , useEffect } from 'react' ;
22import { useNavigate , useLocation , Link } from 'react-router-dom' ;
3+ import { QRCodeSVG } from 'qrcode.react' ;
34import api from '../api' ;
45import { useAuth } from '../App' ;
56
@@ -30,7 +31,7 @@ const ProfilePage = () => {
3031 else setActiveTab ( 'accountDetails' ) ;
3132 } , [ location ] ) ;
3233
33- const [ formData , setFormData ] = useState ( { username : '' , email : '' , password : '' } ) ;
34+ const [ formData , setFormData ] = useState ( { username : '' , email : '' , password : '' , currentPassword : '' , disable2faPassword : '' } ) ;
3435 const [ message , setMessage ] = useState ( { text : '' , type : '' } ) ;
3536 const [ isStripeLoading , setIsStripeLoading ] = useState ( false ) ;
3637
@@ -39,11 +40,17 @@ const ProfilePage = () => {
3940 const [ libraryLoading , setLibraryLoading ] = useState ( false ) ;
4041 const [ libraryError , setLibraryError ] = useState < string | null > ( null ) ;
4142
43+ // 2FA state
44+ const [ is2faEnabled , setIs2faEnabled ] = useState ( false ) ;
45+ const [ setup2faData , setSetup2faData ] = useState < { uri : string , secret : string } | null > ( null ) ;
46+ const [ verify2faCode , setVerify2faCode ] = useState ( '' ) ;
47+
4248 useEffect ( ( ) => { refreshUser ?.( ) ; } , [ ] ) ;
4349
4450 useEffect ( ( ) => {
4551 if ( user ) {
4652 setFormData ( prev => ( { ...prev , username : user . username || '' , email : user . email || '' } ) ) ;
53+ setIs2faEnabled ( user . totp_enabled || false ) ;
4754 }
4855 } , [ user ] ) ;
4956
@@ -109,6 +116,46 @@ const ProfilePage = () => {
109116 }
110117 } ;
111118
119+ const start2faSetup = async ( ) => {
120+ try {
121+ const res = await api . post ( '/user/2fa/setup' ) ;
122+ setSetup2faData ( res . data ) ;
123+ } catch ( err : any ) {
124+ showMessage ( err . response ?. data ?. error || 'Failed to start 2FA setup.' , true ) ;
125+ if ( err . response ?. status === 400 && err . response ?. data ?. error === '2FA is already enabled' ) {
126+ setIs2faEnabled ( true ) ;
127+ refreshUser ?.( ) ;
128+ }
129+ }
130+ } ;
131+
132+ const confirm2faSetup = async ( e : React . FormEvent ) => {
133+ e . preventDefault ( ) ;
134+ try {
135+ await api . post ( '/user/2fa/verify' , { code : verify2faCode } ) ;
136+ setIs2faEnabled ( true ) ;
137+ setSetup2faData ( null ) ;
138+ setVerify2faCode ( '' ) ;
139+ showMessage ( '2FA enabled successfully!' ) ;
140+ refreshUser ?.( ) ;
141+ } catch ( err : any ) {
142+ showMessage ( err . response ?. data ?. error || 'Invalid 2FA code.' , true ) ;
143+ }
144+ } ;
145+
146+ const disable2fa = async ( e : React . FormEvent ) => {
147+ e . preventDefault ( ) ;
148+ try {
149+ await api . post ( '/user/2fa/disable' , { password : formData . disable2faPassword } ) ;
150+ setIs2faEnabled ( false ) ;
151+ setFormData ( { ...formData , disable2faPassword : '' } ) ;
152+ showMessage ( '2FA has been disabled.' ) ;
153+ refreshUser ?.( ) ;
154+ } catch ( err : any ) {
155+ showMessage ( err . response ?. data ?. error || 'Failed to disable 2FA.' , true ) ;
156+ }
157+ } ;
158+
112159 return (
113160 < div className = "container profile-page" >
114161 < h1 className = "page-title" > User < span > Profile</ span > </ h1 >
@@ -328,11 +375,54 @@ const ProfilePage = () => {
328375 { /* ── Security ── */ }
329376 { activeTab === 'security' && (
330377 < div className = "profile-section" >
378+ < h2 className = "section-title" > < span > Two-Factor</ span > Authentication</ h2 >
379+ < div style = { { background : 'var(--surface-light)' , padding : '1.5rem' , borderRadius : '12px' , border : '1px solid var(--border)' , marginBottom : '3rem' } } >
380+ { is2faEnabled ? (
381+ < div >
382+ < p style = { { color : 'var(--success)' , fontWeight : 600 , marginBottom : '1rem' } } > ✓ Two-Factor Authentication is currently enabled.</ p >
383+ < form onSubmit = { disable2fa } style = { { marginTop : '1rem' } } >
384+ < p style = { { fontSize : '0.85rem' , color : 'var(--text-muted)' , marginBottom : '1rem' } } > Enter your password to disable 2FA.</ p >
385+ < div className = "form-group" >
386+ < input type = "password" placeholder = "Current Password" value = { formData . disable2faPassword } onChange = { e => setFormData ( { ...formData , disable2faPassword : e . target . value } ) } required />
387+ </ div >
388+ < button type = "submit" className = "btn btn-secondary" style = { { color : '#ff4d4d' , borderColor : '#ff4d4d' } } > Disable 2FA</ button >
389+ </ form >
390+ </ div >
391+ ) : (
392+ < div >
393+ < p style = { { marginBottom : '1rem' , color : 'var(--text-muted)' } } > Protect your account with Two-Factor Authentication using an app like Google Authenticator or Authy.</ p >
394+ { ! setup2faData ? (
395+ < button className = "btn" onClick = { start2faSetup } > Set Up 2FA</ button >
396+ ) : (
397+ < div style = { { marginTop : '1rem' } } >
398+ < p style = { { fontSize : '0.9rem' , marginBottom : '1rem' } } > 1. Scan this QR code with your authenticator app:</ p >
399+ < div style = { { background : 'white' , padding : '1rem' , display : 'inline-block' , borderRadius : '8px' , marginBottom : '1rem' } } >
400+ < QRCodeSVG value = { setup2faData . uri } size = { 150 } />
401+ </ div >
402+ < p style = { { fontSize : '0.8rem' , color : 'var(--text-muted)' , marginBottom : '1.5rem' } } > Or enter this code manually: < strong style = { { fontFamily : 'var(--font-mono)' } } > { setup2faData . secret } </ strong > </ p >
403+
404+ < form onSubmit = { confirm2faSetup } >
405+ < p style = { { fontSize : '0.9rem' , marginBottom : '0.5rem' } } > 2. Enter the 6-digit code from your app:</ p >
406+ < div className = "form-group" style = { { display : 'flex' , gap : '0.5rem' , maxWidth : '300px' } } >
407+ < input type = "text" placeholder = "000000" maxLength = { 6 } value = { verify2faCode } onChange = { e => setVerify2faCode ( e . target . value . replace ( / \D / g, '' ) ) } required style = { { fontFamily : 'var(--font-mono)' , fontSize : '1.2rem' , letterSpacing : '0.2em' , textAlign : 'center' } } />
408+ < button type = "submit" className = "btn" > Verify</ button >
409+ </ div >
410+ </ form >
411+ </ div >
412+ ) }
413+ </ div >
414+ ) }
415+ </ div >
416+
331417 < h2 className = "section-title" > < span > Change</ span > Password</ h2 >
332- < form onSubmit = { ( e ) => handleUpdate ( e , '/user/settings' , { newPassword : formData . password } ) } >
418+ < form onSubmit = { ( e ) => handleUpdate ( e , '/user/settings' , { currentPassword : formData . currentPassword , newPassword : formData . password } ) } >
419+ < div className = "form-group" >
420+ < label > CURRENT PASSWORD</ label >
421+ < input type = "password" value = { formData . currentPassword } onChange = { e => setFormData ( { ...formData , currentPassword : e . target . value } ) } required />
422+ </ div >
333423 < div className = "form-group" >
334424 < label > NEW PASSWORD</ label >
335- < input type = "password" value = { formData . password } onChange = { e => setFormData ( { ...formData , password : e . target . value } ) } />
425+ < input type = "password" value = { formData . password } onChange = { e => setFormData ( { ...formData , password : e . target . value } ) } required />
336426 </ div >
337427 < button type = "submit" className = "btn" > Update Password</ button >
338428 </ form >
0 commit comments