@ -1,40 +1,40 @@
'use client'
'use client' ;
import { useState } from 'react'
import { useState } from 'react' ;
import { useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next' ;
import { RiShieldKeyholeLine , RiCheckboxCircleFill , RiLoader2Line } from '@remixicon/react'
import { RiShieldKeyholeLine , RiCheckboxCircleFill , RiLoader2Line } from '@remixicon/react' ;
import Toast from '../../base/toast'
import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query' ;
import Button from '../../base/button'
import Toast from '../../base/toast' ;
import Input from '../../base/input'
import Button from '../../base/button' ;
import Modal from '../../base/modal'
import Input from '../../base/input' ;
import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query'
import Modal from '../../base/modal' ;
// API service functions
// API service functions
const mfaService = {
const mfaService = {
getStatus : async ( ) = > {
getStatus : async ( ) = > {
const token = localStorage . getItem ( 'console_token' )
const token = localStorage . getItem ( 'console_token' ) ;
const response = await fetch ( '/console/api/account/mfa/status' , {
const response = await fetch ( '/console/api/account/mfa/status' , {
headers : {
headers : {
'Authorization' : ` Bearer ${ token } ` ,
'Authorization' : ` Bearer ${ token } ` ,
} ,
} ,
} )
} ) ;
if ( ! response . ok ) throw new Error ( 'Failed to fetch MFA status' )
if ( ! response . ok ) throw new Error ( 'Failed to fetch MFA status' ) ;
return response . json ( )
return response . json ( ) ;
} ,
} ,
initSetup : async ( ) = > {
initSetup : async ( ) = > {
const token = localStorage . getItem ( 'console_token' )
const token = localStorage . getItem ( 'console_token' ) ;
const response = await fetch ( '/console/api/account/mfa/setup' , {
const response = await fetch ( '/console/api/account/mfa/setup' , {
method : 'POST' ,
method : 'POST' ,
headers : {
headers : {
'Authorization' : ` Bearer ${ token } ` ,
'Authorization' : ` Bearer ${ token } ` ,
} ,
} ,
} )
} ) ;
if ( ! response . ok ) throw new Error ( 'Failed to initialize MFA setup' )
if ( ! response . ok ) throw new Error ( 'Failed to initialize MFA setup' ) ;
return response . json ( )
return response . json ( ) ;
} ,
} ,
completeSetup : async ( totpToken : string , password : string ) = > {
completeSetup : async ( totpToken : string , password : string ) = > {
const token = localStorage . getItem ( 'console_token' )
const token = localStorage . getItem ( 'console_token' ) ;
const response = await fetch ( '/console/api/account/mfa/setup/complete' , {
const response = await fetch ( '/console/api/account/mfa/setup/complete' , {
method : 'POST' ,
method : 'POST' ,
headers : {
headers : {
@ -42,13 +42,13 @@ const mfaService = {
'Authorization' : ` Bearer ${ token } ` ,
'Authorization' : ` Bearer ${ token } ` ,
} ,
} ,
body : JSON.stringify ( { totp_token : totpToken } ) ,
body : JSON.stringify ( { totp_token : totpToken } ) ,
} )
} ) ;
if ( ! response . ok ) throw new Error ( 'Failed to complete MFA setup' )
if ( ! response . ok ) throw new Error ( 'Failed to complete MFA setup' ) ;
return response . json ( )
return response . json ( ) ;
} ,
} ,
disable : async ( password : string ) = > {
disable : async ( password : string ) = > {
const token = localStorage . getItem ( 'console_token' )
const token = localStorage . getItem ( 'console_token' ) ;
const response = await fetch ( '/console/api/account/mfa/disable' , {
const response = await fetch ( '/console/api/account/mfa/disable' , {
method : 'POST' ,
method : 'POST' ,
headers : {
headers : {
@ -56,98 +56,98 @@ const mfaService = {
'Authorization' : ` Bearer ${ token } ` ,
'Authorization' : ` Bearer ${ token } ` ,
} ,
} ,
body : JSON.stringify ( { password } ) ,
body : JSON.stringify ( { password } ) ,
} )
} ) ;
if ( ! response . ok ) throw new Error ( 'Failed to disable MFA' )
if ( ! response . ok ) throw new Error ( 'Failed to disable MFA' ) ;
return response . json ( )
return response . json ( ) ;
} ,
} ,
}
} ;
export default function MFAPage() {
export default function MFAPage() {
const { t } = useTranslation ( )
const { t } = useTranslation ( ) ;
const queryClient = useQueryClient ( )
const queryClient = useQueryClient ( ) ;
// State
// State
const [ isSetupModalOpen , setIsSetupModalOpen ] = useState ( false )
const [ isSetupModalOpen , setIsSetupModalOpen ] = useState ( false ) ;
const [ isDisableModalOpen , setIsDisableModalOpen ] = useState ( false )
const [ isDisableModalOpen , setIsDisableModalOpen ] = useState ( false ) ;
const [ setupStep , setSetupStep ] = useState < 'qr' | 'verify' | 'backup' > ( 'qr' )
const [ setupStep , setSetupStep ] = useState < 'qr' | 'verify' | 'backup' > ( 'qr' ) ;
const [ totpToken , setTotpToken ] = useState ( '' )
const [ totpToken , setTotpToken ] = useState ( '' ) ;
const [ password , setPassword ] = useState ( '' )
const [ password , setPassword ] = useState ( '' ) ;
const [ qrData , setQrData ] = useState < { secret : string ; qr_code : string } | null > ( null )
const [ qrData , setQrData ] = useState < { secret : string ; qr_code : string } | null > ( null ) ;
const [ backupCodes , setBackupCodes ] = useState < string [ ] > ( [ ] )
const [ backupCodes , setBackupCodes ] = useState < string [ ] > ( [ ] ) ;
// Query MFA status
// Query MFA status
const { data : mfaStatus , isLoading } = useQuery ( {
const { data : mfaStatus , isLoading } = useQuery ( {
queryKey : [ 'mfa-status' ] ,
queryKey : [ 'mfa-status' ] ,
queryFn : mfaService.getStatus ,
queryFn : mfaService.getStatus ,
} )
} ) ;
// Mutations
// Mutations
const initSetupMutation = useMutation ( {
const initSetupMutation = useMutation ( {
mutationFn : mfaService.initSetup ,
mutationFn : mfaService.initSetup ,
onSuccess : ( data ) = > {
onSuccess : ( data ) = > {
setQrData ( data )
setQrData ( data ) ;
setIsSetupModalOpen ( true )
setIsSetupModalOpen ( true ) ;
setSetupStep ( 'qr' )
setSetupStep ( 'qr' ) ;
} ,
} ,
onError : ( ) = > {
onError : ( ) = > {
Toast . notify ( { type : 'error' , message : t ( 'common.somethingWentWrong' ) } )
Toast . notify ( { type : 'error' , message : t ( 'common.somethingWentWrong' ) } ) ;
} ,
} ,
} )
} ) ;
const completeSetupMutation = useMutation ( {
const completeSetupMutation = useMutation ( {
mutationFn : ( { totpToken , password } : { totpToken : string ; password : string } ) = >
mutationFn : ( { totpToken , password } : { totpToken : string ; password : string } ) = >
mfaService . completeSetup ( totpToken , password ) ,
mfaService . completeSetup ( totpToken , password ) ,
onSuccess : ( data ) = > {
onSuccess : ( data ) = > {
setBackupCodes ( data . backup_codes )
setBackupCodes ( data . backup_codes ) ;
setSetupStep ( 'backup' )
setSetupStep ( 'backup' ) ;
queryClient . invalidateQueries ( { queryKey : [ 'mfa-status' ] } )
queryClient . invalidateQueries ( { queryKey : [ 'mfa-status' ] } ) ;
} ,
} ,
onError : ( ) = > {
onError : ( ) = > {
Toast . notify ( { type : 'error' , message : t ( 'mfa.invalidToken' ) } )
Toast . notify ( { type : 'error' , message : t ( 'mfa.invalidToken' ) } ) ;
} ,
} ,
} )
} ) ;
const disableMutation = useMutation ( {
const disableMutation = useMutation ( {
mutationFn : mfaService.disable ,
mutationFn : mfaService.disable ,
onSuccess : ( ) = > {
onSuccess : ( ) = > {
setIsDisableModalOpen ( false )
setIsDisableModalOpen ( false ) ;
queryClient . invalidateQueries ( { queryKey : [ 'mfa-status' ] } )
queryClient . invalidateQueries ( { queryKey : [ 'mfa-status' ] } ) ;
Toast . notify ( { type : 'success' , message : t ( 'mfa.disabledSuccess' ) } )
Toast . notify ( { type : 'success' , message : t ( 'mfa.disabledSuccess' ) } ) ;
} ,
} ,
onError : ( ) = > {
onError : ( ) = > {
Toast . notify ( { type : 'error' , message : t ( 'mfa.invalidPassword' ) } )
Toast . notify ( { type : 'error' , message : t ( 'mfa.invalidPassword' ) } ) ;
} ,
} ,
} )
} ) ;
const handleSetupStart = ( ) = > {
const handleSetupStart = ( ) = > {
initSetupMutation . mutate ( )
initSetupMutation . mutate ( ) ;
}
} ;
const handleVerifyToken = ( ) = > {
const handleVerifyToken = ( ) = > {
if ( totpToken . length !== 6 ) {
if ( totpToken . length !== 6 ) {
Toast . notify ( { type : 'error' , message : t ( 'mfa.tokenLength' ) } )
Toast . notify ( { type : 'error' , message : t ( 'mfa.tokenLength' ) } ) ;
return
return ;
}
completeSetupMutation . mutate ( { totpToken , password : '' } )
}
}
completeSetupMutation . mutate ( { totpToken , password : '' } ) ;
} ;
const handleDisable = ( ) = > {
const handleDisable = ( ) = > {
disableMutation . mutate ( password )
disableMutation . mutate ( password ) ;
}
} ;
const handleCopyBackupCodes = ( ) = > {
const handleCopyBackupCodes = ( ) = > {
const codesText = backupCodes . join ( '\n' )
const codesText = backupCodes . join ( '\n' ) ;
navigator . clipboard . writeText ( codesText )
navigator . clipboard . writeText ( codesText ) ;
Toast . notify ( { type : 'success' , message : t ( 'mfa.copied' ) } )
Toast . notify ( { type : 'success' , message : t ( 'mfa.copied' ) } ) ;
}
} ;
if ( isLoading ) {
if ( isLoading ) {
return (
return (
< div className = "flex items-center justify-center h-96" >
< div className = "flex items-center justify-center h-96" >
< RiLoader2Line className = "animate-spin w-6 h-6 text-text-tertiary" / >
< RiLoader2Line className = "animate-spin w-6 h-6 text-text-tertiary" / >
< / div >
< / div >
)
) ;
}
}
return (
return (
@ -274,8 +274,8 @@ export default function MFAPage() {
variant = "primary"
variant = "primary"
className = "flex-1"
className = "flex-1"
onClick = { ( ) = > {
onClick = { ( ) = > {
setIsSetupModalOpen ( false )
setIsSetupModalOpen ( false ) ;
Toast . notify ( { type : 'success' , message : t ( 'mfa.enabledSuccess' ) } )
Toast . notify ( { type : 'success' , message : t ( 'mfa.enabledSuccess' ) } ) ;
} }
} }
>
>
{ t ( 'mfa.done' ) }
{ t ( 'mfa.done' ) }
@ -321,5 +321,5 @@ export default function MFAPage() {
< / div >
< / div >
< / Modal >
< / Modal >
< / div >
< / div >
)
) ;
}
}