2fa
This commit is contained in:
+24
-2
@@ -25,7 +25,8 @@
|
||||
"invalid_email": "فرمت ایمیل نامعتبر است"
|
||||
},
|
||||
"common": {
|
||||
"close": "بستن"
|
||||
"close": "بستن",
|
||||
"loading": "در حال بارگذاری..."
|
||||
},
|
||||
"profile": {
|
||||
"account_user": "حساب کاربری",
|
||||
@@ -128,7 +129,28 @@
|
||||
"INVOICE": "صورتحساب",
|
||||
"ACCOUNT": "حساب",
|
||||
"NOTIFICATION": "اعلان",
|
||||
"FINANCE": "مالی"
|
||||
"FINANCE": "مالی",
|
||||
"2fa": "دو مرحله ای",
|
||||
"2fa_setup": "راهاندازی احراز هویت دو مرحلهای",
|
||||
"2fa_description": "احراز هویت دو مرحلهای امنیت اکانت شما را افزایش میدهد",
|
||||
"setup_2fa": "راهاندازی کد دو مرحلهای",
|
||||
"scan_qr_code": "QR Code زیر را با برنامه Authenticator اسکن کنید",
|
||||
"manual_entry": "یا این کد را به صورت دستی وارد کنید",
|
||||
"enter_verification_code": "کد تایید 6 رقمی را وارد کنید",
|
||||
"enable_2fa": "فعال سازی احراز هویت دو مرحلهای",
|
||||
"2fa_enabled_successfully": "احراز هویت دو مرحلهای با موفقیت فعال شد",
|
||||
"2fa_enabled_description": "اکانت شما اکنون محافظت اضافی دارد",
|
||||
"invalid_code": "کد وارد شده نامعتبر است",
|
||||
"invalid_code_length": "کد باید 6 رقم باشد",
|
||||
"setup_failed": "خطا در راهاندازی. دوباره تلاش کنید",
|
||||
"2fa_setup_description": "برای شروع فرآیند راهاندازی احراز هویت دو مرحلهای، روی دکمه زیر کلیک کنید",
|
||||
"2fa_info_title": "راهنمای احراز هویت دو مرحلهای",
|
||||
"2fa_info_description": "این قابلیت امنیت حساب شما را به شدت افزایش میدهد",
|
||||
"2fa_step_1": "یک برنامه Authenticator مانند Google Authenticator نصب کنید",
|
||||
"2fa_step_2": "QR Code را اسکن کنید یا کد را به صورت دستی وارد کنید",
|
||||
"2fa_step_3": "کد 6 رقمی تولید شده توسط برنامه را وارد کنید",
|
||||
"note": "توجه",
|
||||
"2fa_note": "کد های پشتیبان خود را در مکانی امن نگهداری کنید"
|
||||
},
|
||||
"success": "عملیات با موفقیت انجام شد",
|
||||
"attachment_select": "اضافه کردن فایل ضمیمه",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { KeySquare, Notification1 } from 'iconsax-react'
|
||||
import { useGetSettings } from './hooks/useSettingData'
|
||||
import Sms from './components/Sms'
|
||||
import ChangePassword from './components/ChangePassword'
|
||||
import TwoFactor from './components/TwoFactor'
|
||||
import TitleLine from '@/components/TitleLine'
|
||||
import { SettingCategoryType, SettingItemType } from './types/SettingTypes'
|
||||
|
||||
@@ -33,6 +34,11 @@ const Setting: FC = () => {
|
||||
label: t('setting.password'),
|
||||
value: 'password'
|
||||
},
|
||||
{
|
||||
icon: <KeySquare color={activeTab === '2fa' ? 'black' : '#8C90A3'} size={22} />,
|
||||
label: t('setting.2fa'),
|
||||
value: '2fa'
|
||||
},
|
||||
]}
|
||||
active={activeTab}
|
||||
onChange={setActiveTab}
|
||||
@@ -90,8 +96,11 @@ const Setting: FC = () => {
|
||||
<div className='flex-1 px-4 min-w-[40%]'></div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<ChangePassword />
|
||||
: activeTab === 'password' ?
|
||||
<ChangePassword />
|
||||
: activeTab === '2fa' ?
|
||||
<TwoFactor />
|
||||
: null
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSetupTwoFactor, useEnableTwoFactor } from '../hooks/useSettingData'
|
||||
import Input from '../../../components/Input'
|
||||
import Button from '../../../components/Button'
|
||||
import { toast } from '../../../components/Toast'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { TickCircle, Scan, Key } from 'iconsax-react'
|
||||
import { TwoFactorSetupResponseType } from '../types/SettingTypes'
|
||||
import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
|
||||
|
||||
const TwoFactor: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { data: profile } = useGetProfile()
|
||||
const [qrData, setQrData] = useState<TwoFactorSetupResponseType['data'] | null>(null)
|
||||
const [token, setToken] = useState('')
|
||||
const [isEnabled, setIsEnabled] = useState(false)
|
||||
|
||||
const setupMutation = useSetupTwoFactor()
|
||||
const enableMutation = useEnableTwoFactor()
|
||||
|
||||
const handleSetup = () => {
|
||||
setupMutation.mutate(undefined, {
|
||||
onSuccess: (data) => {
|
||||
setQrData(data.data)
|
||||
toast(data.data.message, 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
const errorMessage = error.response?.data?.error?.message
|
||||
const message = Array.isArray(errorMessage) ? errorMessage[0] : errorMessage
|
||||
toast(message || t('setting.setup_failed'), 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (profile?.data?.user?.is2FAEnabled) {
|
||||
setIsEnabled(true)
|
||||
}
|
||||
}, [profile])
|
||||
|
||||
const handleEnable = () => {
|
||||
if (token.length !== 6) {
|
||||
toast(t('setting.invalid_code_length'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
enableMutation.mutate(token, {
|
||||
onSuccess: (data) => {
|
||||
setIsEnabled(true)
|
||||
setQrData(null)
|
||||
setToken('')
|
||||
toast(data.message || t('setting.2fa_enabled_successfully'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
const errorMessage = error.response?.data?.error?.message
|
||||
const message = Array.isArray(errorMessage) ? errorMessage[0] : errorMessage
|
||||
toast(message || t('setting.invalid_code'), 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, '').slice(0, 6)
|
||||
setToken(value)
|
||||
}
|
||||
|
||||
if (isEnabled) {
|
||||
return (
|
||||
<div className='mt-10 bg-white xl:p-8 p-4 rounded-3xl'>
|
||||
<div className='text-center py-8'>
|
||||
<div className='inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4'>
|
||||
<TickCircle size={32} color='#22c55e' />
|
||||
</div>
|
||||
<h2 className='text-lg font-semibold text-green-600 mb-2'>
|
||||
{t('setting.2fa_enabled_successfully')}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-10 bg-white xl:p-8 p-4 rounded-3xl'>
|
||||
<div className='mb-6 border-b border-border pb-4'>
|
||||
<h2>
|
||||
{t('setting.2fa_setup')}
|
||||
</h2>
|
||||
<p className='text-xs mt-2 text-description'>
|
||||
{t('setting.2fa_description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex xl:flex-row flex-col xl:gap-10 gap-6'>
|
||||
{/* Setup Section */}
|
||||
<div className='flex-1'>
|
||||
{!qrData ? (
|
||||
<div className='text-center py-8'>
|
||||
<div className='inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4'>
|
||||
<Key size={32} color='#3b82f6' />
|
||||
</div>
|
||||
<p className='text-sm text-gray-600 mb-6'>
|
||||
{t('setting.2fa_setup_description')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleSetup}
|
||||
loading={setupMutation.isPending}
|
||||
className='!px-14 w-fit'
|
||||
>
|
||||
<div className='flex gap-2 justify-center items-center'>
|
||||
<Scan size={20} color='white' />
|
||||
<span>{t('setting.setup_2fa')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-6'>
|
||||
{/* QR Code Section */}
|
||||
<div className='text-center'>
|
||||
<h3 className='text-md font-medium mb-4'>
|
||||
{t('setting.scan_qr_code')}
|
||||
</h3>
|
||||
<div className='flex justify-center mb-4'>
|
||||
<div className='p-4 bg-white border-2 border-gray-200 rounded-xl shadow-sm'>
|
||||
<img
|
||||
src={qrData.qr}
|
||||
alt="QR Code"
|
||||
className='w-48 h-48'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 bg-gray-50 p-3 rounded-lg'>
|
||||
<div className='font-medium mb-1'>{t('setting.manual_entry')}:</div>
|
||||
<div className='font-mono break-all text-gray-700'>{qrData.seed}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Input Section */}
|
||||
<div className='space-y-4'>
|
||||
<Input
|
||||
type='text'
|
||||
value={token}
|
||||
onChange={handleTokenChange}
|
||||
placeholder='123456'
|
||||
className='text-center text-lg font-mono'
|
||||
maxLength={6}
|
||||
label={t('setting.enter_verification_code')}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleEnable}
|
||||
disabled={token.length !== 6}
|
||||
loading={enableMutation.isPending}
|
||||
className='w-full'
|
||||
>
|
||||
<div className='flex gap-2 items-center justify-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<span>{t('setting.enable_2fa')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Information Section */}
|
||||
<div className='flex-1 xl:p-8 p-4 bg-[#EEF0F7] rounded-3xl text-sm'>
|
||||
<div className='mb-4'>
|
||||
<h3 className='font-medium mb-2'>{t('setting.2fa_info_title')}</h3>
|
||||
<p className='text-xs text-gray-600'>
|
||||
{t('setting.2fa_info_description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-start gap-3'>
|
||||
<div className='w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
|
||||
<span className='text-xs font-medium text-blue-600'>1</span>
|
||||
</div>
|
||||
<p className='text-xs'>{t('setting.2fa_step_1')}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-start gap-3'>
|
||||
<div className='w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
|
||||
<span className='text-xs font-medium text-blue-600'>2</span>
|
||||
</div>
|
||||
<p className='text-xs'>{t('setting.2fa_step_2')}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-start gap-3'>
|
||||
<div className='w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
|
||||
<span className='text-xs font-medium text-blue-600'>3</span>
|
||||
</div>
|
||||
<p className='text-xs'>{t('setting.2fa_step_3')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 p-3 bg-yellow-50 border border-yellow-200 rounded-lg'>
|
||||
<p className='text-xs text-yellow-800'>
|
||||
<span className='font-medium'>{t('setting.note')}:</span> {t('setting.2fa_note')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='h-14'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TwoFactor
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/SettingService";
|
||||
import { ChangePasswordType, UpdateSettingType } from "../types/SettingTypes";
|
||||
import {
|
||||
ChangePasswordType,
|
||||
UpdateSettingType,
|
||||
TwoFactorSetupResponseType,
|
||||
TwoFactorEnableResponseType,
|
||||
} from "../types/SettingTypes";
|
||||
|
||||
export const useGetSettings = () => {
|
||||
return useQuery({
|
||||
@@ -21,3 +26,15 @@ export const useChangePassword = () => {
|
||||
api.changePassword(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSetupTwoFactor = () => {
|
||||
return useMutation<TwoFactorSetupResponseType>({
|
||||
mutationFn: () => api.setupTwoFactor(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useEnableTwoFactor = () => {
|
||||
return useMutation<TwoFactorEnableResponseType, Error, string>({
|
||||
mutationFn: (token: string) => api.enableTwoFactor(token),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { ChangePasswordType, UpdateSettingType } from "../types/SettingTypes";
|
||||
import {
|
||||
ChangePasswordType,
|
||||
UpdateSettingType,
|
||||
TwoFactorSetupResponseType,
|
||||
TwoFactorEnableResponseType,
|
||||
} from "../types/SettingTypes";
|
||||
|
||||
export const getSettings = async () => {
|
||||
const { data } = await axios.get(`/settings`);
|
||||
@@ -13,3 +18,15 @@ export const updateSettings = async (params: UpdateSettingType) => {
|
||||
export const changePassword = async (params: ChangePasswordType) => {
|
||||
await axios.patch(`/auth/change-password`, params);
|
||||
};
|
||||
|
||||
export const setupTwoFactor = async (): Promise<TwoFactorSetupResponseType> => {
|
||||
const { data } = await axios.post(`/auth/2fa/setup`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const enableTwoFactor = async (
|
||||
token: string
|
||||
): Promise<TwoFactorEnableResponseType> => {
|
||||
const { data } = await axios.post(`/auth/2fa/enable`, { token });
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -29,3 +29,23 @@ export type SettingsResponseType = {
|
||||
settings: SettingCategoryType[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TwoFactorSetupResponseType = {
|
||||
statusCode: number;
|
||||
success: boolean;
|
||||
data: {
|
||||
message: string;
|
||||
qr: string;
|
||||
seed: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TwoFactorEnableResponseType = {
|
||||
statusCode: number;
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type TwoFactorEnableType = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user