2fa
This commit is contained in:
+24
-2
@@ -25,7 +25,8 @@
|
|||||||
"invalid_email": "فرمت ایمیل نامعتبر است"
|
"invalid_email": "فرمت ایمیل نامعتبر است"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "بستن"
|
"close": "بستن",
|
||||||
|
"loading": "در حال بارگذاری..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"account_user": "حساب کاربری",
|
"account_user": "حساب کاربری",
|
||||||
@@ -128,7 +129,28 @@
|
|||||||
"INVOICE": "صورتحساب",
|
"INVOICE": "صورتحساب",
|
||||||
"ACCOUNT": "حساب",
|
"ACCOUNT": "حساب",
|
||||||
"NOTIFICATION": "اعلان",
|
"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": "عملیات با موفقیت انجام شد",
|
"success": "عملیات با موفقیت انجام شد",
|
||||||
"attachment_select": "اضافه کردن فایل ضمیمه",
|
"attachment_select": "اضافه کردن فایل ضمیمه",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { KeySquare, Notification1 } from 'iconsax-react'
|
|||||||
import { useGetSettings } from './hooks/useSettingData'
|
import { useGetSettings } from './hooks/useSettingData'
|
||||||
import Sms from './components/Sms'
|
import Sms from './components/Sms'
|
||||||
import ChangePassword from './components/ChangePassword'
|
import ChangePassword from './components/ChangePassword'
|
||||||
|
import TwoFactor from './components/TwoFactor'
|
||||||
import TitleLine from '@/components/TitleLine'
|
import TitleLine from '@/components/TitleLine'
|
||||||
import { SettingCategoryType, SettingItemType } from './types/SettingTypes'
|
import { SettingCategoryType, SettingItemType } from './types/SettingTypes'
|
||||||
|
|
||||||
@@ -33,6 +34,11 @@ const Setting: FC = () => {
|
|||||||
label: t('setting.password'),
|
label: t('setting.password'),
|
||||||
value: 'password'
|
value: 'password'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <KeySquare color={activeTab === '2fa' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
|
label: t('setting.2fa'),
|
||||||
|
value: '2fa'
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
active={activeTab}
|
active={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
@@ -90,8 +96,11 @@ const Setting: FC = () => {
|
|||||||
<div className='flex-1 px-4 min-w-[40%]'></div>
|
<div className='flex-1 px-4 min-w-[40%]'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
: activeTab === 'password' ?
|
||||||
<ChangePassword />
|
<ChangePassword />
|
||||||
|
: activeTab === '2fa' ?
|
||||||
|
<TwoFactor />
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</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 { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import * as api from "../service/SettingService";
|
import * as api from "../service/SettingService";
|
||||||
import { ChangePasswordType, UpdateSettingType } from "../types/SettingTypes";
|
import {
|
||||||
|
ChangePasswordType,
|
||||||
|
UpdateSettingType,
|
||||||
|
TwoFactorSetupResponseType,
|
||||||
|
TwoFactorEnableResponseType,
|
||||||
|
} from "../types/SettingTypes";
|
||||||
|
|
||||||
export const useGetSettings = () => {
|
export const useGetSettings = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -21,3 +26,15 @@ export const useChangePassword = () => {
|
|||||||
api.changePassword(variables),
|
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 axios from "../../../config/axios";
|
||||||
import { ChangePasswordType, UpdateSettingType } from "../types/SettingTypes";
|
import {
|
||||||
|
ChangePasswordType,
|
||||||
|
UpdateSettingType,
|
||||||
|
TwoFactorSetupResponseType,
|
||||||
|
TwoFactorEnableResponseType,
|
||||||
|
} from "../types/SettingTypes";
|
||||||
|
|
||||||
export const getSettings = async () => {
|
export const getSettings = async () => {
|
||||||
const { data } = await axios.get(`/settings`);
|
const { data } = await axios.get(`/settings`);
|
||||||
@@ -13,3 +18,15 @@ export const updateSettings = async (params: UpdateSettingType) => {
|
|||||||
export const changePassword = async (params: ChangePasswordType) => {
|
export const changePassword = async (params: ChangePasswordType) => {
|
||||||
await axios.patch(`/auth/change-password`, params);
|
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[];
|
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