diff --git a/src/langs/fa.json b/src/langs/fa.json index d51405d..5caaf51 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -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": "اضافه کردن فایل ضمیمه", diff --git a/src/pages/setting/Setting.tsx b/src/pages/setting/Setting.tsx index 5768862..06fb497 100644 --- a/src/pages/setting/Setting.tsx +++ b/src/pages/setting/Setting.tsx @@ -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: , + label: t('setting.2fa'), + value: '2fa' + }, ]} active={activeTab} onChange={setActiveTab} @@ -90,8 +96,11 @@ const Setting: FC = () => {
- : - + : activeTab === 'password' ? + + : activeTab === '2fa' ? + + : null } diff --git a/src/pages/setting/components/TwoFactor.tsx b/src/pages/setting/components/TwoFactor.tsx new file mode 100644 index 0000000..4214d38 --- /dev/null +++ b/src/pages/setting/components/TwoFactor.tsx @@ -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(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) => { + const value = e.target.value.replace(/\D/g, '').slice(0, 6) + setToken(value) + } + + if (isEnabled) { + return ( +
+
+
+ +
+

+ {t('setting.2fa_enabled_successfully')} +

+
+
+ ) + } + + return ( +
+
+

+ {t('setting.2fa_setup')} +

+

+ {t('setting.2fa_description')} +

+
+ +
+ {/* Setup Section */} +
+ {!qrData ? ( +
+
+ +
+

+ {t('setting.2fa_setup_description')} +

+ +
+ ) : ( +
+ {/* QR Code Section */} +
+

+ {t('setting.scan_qr_code')} +

+
+
+ QR Code +
+
+
+
{t('setting.manual_entry')}:
+
{qrData.seed}
+
+
+ + {/* Token Input Section */} +
+ + + +
+
+ )} +
+ + {/* Information Section */} +
+
+

{t('setting.2fa_info_title')}

+

+ {t('setting.2fa_info_description')} +

+
+ +
+
+
+ 1 +
+

{t('setting.2fa_step_1')}

+
+ +
+
+ 2 +
+

{t('setting.2fa_step_2')}

+
+ +
+
+ 3 +
+

{t('setting.2fa_step_3')}

+
+
+ +
+

+ {t('setting.note')}: {t('setting.2fa_note')} +

+
+
+
+ +
+
+ ) +} + +export default TwoFactor \ No newline at end of file diff --git a/src/pages/setting/hooks/useSettingData.ts b/src/pages/setting/hooks/useSettingData.ts index 00811b1..1c7bd17 100644 --- a/src/pages/setting/hooks/useSettingData.ts +++ b/src/pages/setting/hooks/useSettingData.ts @@ -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({ + mutationFn: () => api.setupTwoFactor(), + }); +}; + +export const useEnableTwoFactor = () => { + return useMutation({ + mutationFn: (token: string) => api.enableTwoFactor(token), + }); +}; diff --git a/src/pages/setting/service/SettingService.ts b/src/pages/setting/service/SettingService.ts index 5c4dd5c..847e0f0 100644 --- a/src/pages/setting/service/SettingService.ts +++ b/src/pages/setting/service/SettingService.ts @@ -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 => { + const { data } = await axios.post(`/auth/2fa/setup`); + return data; +}; + +export const enableTwoFactor = async ( + token: string +): Promise => { + const { data } = await axios.post(`/auth/2fa/enable`, { token }); + return data; +}; diff --git a/src/pages/setting/types/SettingTypes.ts b/src/pages/setting/types/SettingTypes.ts index a8bd792..f669408 100644 --- a/src/pages/setting/types/SettingTypes.ts +++ b/src/pages/setting/types/SettingTypes.ts @@ -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; +};