diff --git a/src/assets/svg/lock.svg b/src/assets/svg/lock.svg
new file mode 100644
index 0000000..54890e2
--- /dev/null
+++ b/src/assets/svg/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/safe.svg b/src/assets/svg/safe.svg
new file mode 100644
index 0000000..5f19ab9
--- /dev/null
+++ b/src/assets/svg/safe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/svg/unlock.svg b/src/assets/svg/unlock.svg
new file mode 100644
index 0000000..6f740bb
--- /dev/null
+++ b/src/assets/svg/unlock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/constants/common.constants.ts b/src/constants/common.constants.ts
index 29052c6..6a4a6fe 100644
--- a/src/constants/common.constants.ts
+++ b/src/constants/common.constants.ts
@@ -61,6 +61,10 @@ export const PERMISSION_ACCOUNT_NEED_INIT = 20064
export const PERMISSION_USER_NOT_FOUND = 20065
export const PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED = 20066
export const PERMISSION_ACCOUNT_NEED_RESET_PASSWORD = 20067
+export const PERMISSION_NEED_TWO_FACTOR = 20068
+export const PERMISSION_ALREADY_HAS_TWO_FACTOR = 20069
+export const PERMISSION_NO_TWO_FACTOR_FOUND = 20070
+export const PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR = 20071
export const DATABASE_SELECT_SUCCESS = 30000
export const DATABASE_SELECT_FAILED = 30005
diff --git a/src/constants/urls.constants.ts b/src/constants/urls.constants.ts
index ae9b637..615dab8 100644
--- a/src/constants/urls.constants.ts
+++ b/src/constants/urls.constants.ts
@@ -6,6 +6,7 @@ export const URL_RETRIEVE = '/retrieve'
export const URL_LOGIN = '/login'
export const URL_TOKEN = '/token'
export const URL_LOGOUT = '/logout'
+export const URL_TWO_FACTOR = '/two-factor'
export const URL_SYS_LOG = '/system/log'
export const URL_SYS_USER_INFO = '/system/user/info'
export const URL_SYS_USER = '/system/user'
@@ -18,6 +19,7 @@ export const URL_SYS_SETTINGS = '/system/settings'
export const URL_SYS_SETTINGS_BASE = `${URL_SYS_SETTINGS}/base`
export const URL_SYS_SETTINGS_MAIL = `${URL_SYS_SETTINGS}/mail`
export const URL_SYS_SETTINGS_SENSITIVE = `${URL_SYS_SETTINGS}/sensitive`
+export const URL_SYS_SETTINGS_TWO_FACTOR = `${URL_SYS_SETTINGS}/two-factor`
export const URL_SYS_STATISTICS = '/system/statistics'
export const URL_SYS_STATISTICS_SOFTWARE = `${URL_SYS_STATISTICS}/software`
export const URL_SYS_STATISTICS_HARDWARE = `${URL_SYS_STATISTICS}/hardware`
diff --git a/src/global.d.ts b/src/global.d.ts
index 8562706..be92e36 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -92,6 +92,7 @@ interface LoginParam {
account: string
password: string
captchaCode: string
+ twoFactorCode?: string
}
interface UserChangePasswordParam {
@@ -99,9 +100,18 @@ interface UserChangePasswordParam {
newPassword: string
}
+interface TwoFactorValidateParam {
+ code: string
+}
+
+interface TwoFactorRemoveParam {
+ code: string
+}
+
interface UserWithInfoVo {
id: string
username: string
+ twoFactor: boolean
verified: boolean
locking: boolean
expiration: string
@@ -119,6 +129,7 @@ interface UserWithInfoVo {
interface UserWithPowerInfoVo {
id: string
username: string
+ twoFactor: boolean
verified: boolean
locking: boolean
expiration: string
@@ -140,6 +151,7 @@ interface UserWithPowerInfoVo {
interface UserWithRoleInfoVo {
id: string
username: string
+ twoFactor: boolean
verify: string
locking: boolean
expiration: string
@@ -164,6 +176,10 @@ interface UserInfoVo {
email: string
}
+interface TwoFactorVo {
+ qrCodeSVGBase64: string
+}
+
interface ModuleVo {
id: number
name: string
@@ -402,6 +418,16 @@ interface SensitiveWordUpdateParam {
ids: string[]
}
+interface TwoFactorSettingsVo {
+ issuer: string
+ secretKeyLength: number
+}
+
+interface TwoFactorSettingsParam {
+ issuer: string
+ secretKeyLength: number
+}
+
interface SoftwareInfoVo {
os: string
bitness: number
diff --git a/src/pages/Sign/SignIn.tsx b/src/pages/Sign/SignIn.tsx
index 11d86f0..5e7dfa3 100644
--- a/src/pages/Sign/SignIn.tsx
+++ b/src/pages/Sign/SignIn.tsx
@@ -4,6 +4,8 @@ import {
H_CAPTCHA_SITE_KEY,
PERMISSION_LOGIN_SUCCESS,
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
+ PERMISSION_NEED_TWO_FACTOR,
+ PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR,
PERMISSION_USER_DISABLE,
PERMISSION_USERNAME_NOT_FOUND,
SYSTEM_INVALID_CAPTCHA_CODE
@@ -16,6 +18,7 @@ import FitCenter from '@/components/common/FitCenter'
import FlexBox from '@/components/common/FlexBox'
const SignIn = () => {
+ const [modal, contextHolder] = AntdModal.useModal()
const { refreshRouter } = useContext(AppContext)
const navigate = useNavigate()
const [searchParams] = useSearchParams()
@@ -29,6 +32,7 @@ const SignIn = () => {
},
[location.pathname]
)
+ const [twoFactorForm] = AntdForm.useForm<{ twoFactorCode: string }>()
const [isSigningIn, setIsSigningIn] = useState(false)
const [captchaCode, setCaptchaCode] = useState('')
@@ -55,7 +59,8 @@ const SignIn = () => {
void r_auth_login({
account: loginParam.account,
password: loginParam.password,
- captchaCode
+ captchaCode,
+ twoFactorCode: loginParam.twoFactorCode
})
.then((res) => {
const response = res.data
@@ -96,6 +101,56 @@ const SignIn = () => {
})
}, 1500)
break
+ case PERMISSION_NEED_TWO_FACTOR:
+ twoFactorForm.resetFields()
+ void modal.confirm({
+ title: '双因素验证',
+ getContainer: false,
+ centered: true,
+ content: (
+ <>
+
+
+ {
+ input?.focus()
+ }}
+ />
+
+
+ >
+ ),
+ onOk: () =>
+ twoFactorForm.validateFields().then(
+ () => {
+ return new Promise((resolve) => {
+ handleOnFinish({
+ ...loginParam,
+ twoFactorCode: twoFactorForm.getFieldValue(
+ 'twoFactorCode'
+ ) as string
+ })
+ resolve()
+ })
+ },
+ () => {
+ return new Promise((_, reject) => {
+ reject('输入有误')
+ })
+ }
+ ),
+ onCancel: () => {
+ setIsSigningIn(false)
+ }
+ })
+ break
case PERMISSION_USERNAME_NOT_FOUND:
case PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR:
void message.error(
@@ -105,6 +160,10 @@ const SignIn = () => {
)
setIsSigningIn(false)
break
+ case PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR:
+ void message.error('双因素验证码错误')
+ setIsSigningIn(false)
+ break
case PERMISSION_USER_DISABLE:
void message.error(
<>
@@ -170,7 +229,13 @@ const SignIn = () => {
/>
- 记住密码
+ {
+ navigate('/')
+ }}
+ >
+ 返回首页
+
{
navigate(`/forget${location.search}`, { replace: true })
@@ -203,6 +268,7 @@ const SignIn = () => {
+ {contextHolder}
)
}
diff --git a/src/pages/System/Settings/Base.tsx b/src/pages/System/Settings/Base.tsx
index 969e215..aa473c6 100644
--- a/src/pages/System/Settings/Base.tsx
+++ b/src/pages/System/Settings/Base.tsx
@@ -46,7 +46,7 @@ const Base = () => {
return (
<>
{
const handleOnTest = () => {
void modal.confirm({
title: '发送测试邮件',
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
<>
@@ -27,7 +29,11 @@ const Mail = () => {
style={{ marginTop: 10 }}
rules={[{ required: true, type: 'email' }]}
>
-
+ {
+ input?.focus()
+ }}
+ />
@@ -56,7 +62,7 @@ const Mail = () => {
},
() => {
return new Promise((_, reject) => {
- reject('未输入接收者')
+ reject('输入有误')
})
}
)
diff --git a/src/pages/System/Settings/TwoFactor.tsx b/src/pages/System/Settings/TwoFactor.tsx
new file mode 100644
index 0000000..a94e8fc
--- /dev/null
+++ b/src/pages/System/Settings/TwoFactor.tsx
@@ -0,0 +1,73 @@
+import { hasPermission } from '@/util/auth'
+import { r_sys_settings_two_factor_get, r_sys_settings_two_factor_update } from '@/services/system'
+import { SettingsCard } from '@/pages/System/Settings'
+
+const TwoFactor = () => {
+ const [twoFactorForm] = AntdForm.useForm()
+ const twoFactorFormValues = AntdForm.useWatch([], twoFactorForm)
+ const [loading, setLoading] = useState(false)
+
+ const handleOnReset = () => {
+ getTwoFactorSettings()
+ }
+
+ const handleOnSave = () => {
+ void r_sys_settings_two_factor_update(twoFactorFormValues).then((res) => {
+ const response = res.data
+ if (response.success) {
+ void message.success('保存设置成功')
+ getTwoFactorSettings()
+ } else {
+ void message.error('保存设置失败,请稍后重试')
+ }
+ })
+ }
+
+ const getTwoFactorSettings = () => {
+ if (loading) {
+ return
+ }
+ setLoading(true)
+
+ void r_sys_settings_two_factor_get().then((res) => {
+ const response = res.data
+ if (response.success) {
+ const data = response.data
+ data && twoFactorForm.setFieldsValue(data)
+ setLoading(false)
+ }
+ })
+ }
+
+ useEffect(() => {
+ getTwoFactorSettings()
+ }, [])
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default TwoFactor
diff --git a/src/pages/System/Settings/index.tsx b/src/pages/System/Settings/index.tsx
index ac2d4c6..94cba62 100644
--- a/src/pages/System/Settings/index.tsx
+++ b/src/pages/System/Settings/index.tsx
@@ -10,6 +10,7 @@ import Permission from '@/components/common/Permission'
import Base from '@/pages/System/Settings/Base'
import Mail from '@/pages/System/Settings/Mail'
import SensitiveWord from '@/pages/System/Settings/SensitiveWord'
+import TwoFactor from '@/pages/System/Settings/TwoFactor.tsx'
interface SettingsCardProps extends PropsWithChildren {
icon: IconComponent
@@ -68,6 +69,9 @@ const Settings = () => {
+
+
+
diff --git a/src/pages/System/Tools/Base.tsx b/src/pages/System/Tools/Base.tsx
index e57891e..6e7a566 100644
--- a/src/pages/System/Tools/Base.tsx
+++ b/src/pages/System/Tools/Base.tsx
@@ -232,6 +232,7 @@ const Base = () => {
compileForm.setFieldValue('entryFileName', undefined)
void modal.confirm({
title: '编译',
+ centered: true,
maskClosable: true,
content: (
<>
@@ -537,6 +538,8 @@ const Base = () => {
const handleOnAddFile = () => {
void modal.confirm({
title: '新建文件',
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
@@ -565,7 +568,11 @@ const Base = () => {
})
]}
>
-
+ {
+ input?.focus()
+ }}
+ />
),
@@ -714,6 +721,8 @@ const Base = () => {
renameFileForm.setFieldValue('fileName', fileName)
void modal.confirm({
title: '重命名文件',
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
@@ -745,7 +754,11 @@ const Base = () => {
})
]}
>
-
+ {
+ input?.focus()
+ }}
+ />
),
diff --git a/src/pages/System/Tools/Template.tsx b/src/pages/System/Tools/Template.tsx
index f6985ac..330f9a8 100644
--- a/src/pages/System/Tools/Template.tsx
+++ b/src/pages/System/Tools/Template.tsx
@@ -399,6 +399,8 @@ const Template = () => {
const handleOnAddFile = () => {
void modal.confirm({
title: '新建文件',
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
@@ -427,7 +429,11 @@ const Template = () => {
})
]}
>
-
+ {
+ input?.focus()
+ }}
+ />
),
@@ -579,6 +585,8 @@ const Template = () => {
renameFileForm.setFieldValue('fileName', fileName)
void modal.confirm({
title: '重命名文件',
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
@@ -610,7 +618,11 @@ const Template = () => {
})
]}
>
-
+ {
+ input?.focus()
+ }}
+ />
),
diff --git a/src/pages/System/Tools/index.tsx b/src/pages/System/Tools/index.tsx
index 26b0aa4..8eef2d1 100644
--- a/src/pages/System/Tools/index.tsx
+++ b/src/pages/System/Tools/index.tsx
@@ -195,6 +195,7 @@ const Tools = () => {
form.setFieldValue('pass', undefined)
void modal.confirm({
title: '审核',
+ centered: true,
maskClosable: true,
content: (
diff --git a/src/pages/System/User.tsx b/src/pages/System/User.tsx
index d4a4ea2..3b18a2b 100644
--- a/src/pages/System/User.tsx
+++ b/src/pages/System/User.tsx
@@ -336,6 +336,8 @@ const User = () => {
修改用户 {value.username} 的密码
>
),
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
{
}
]}
>
-
+ {
+ input?.focus()
+ }}
+ />
{
const handleOnUpgradeTool = (tool: ToolVo) => {
void modal.confirm({
title: '更新工具',
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
<>
@@ -285,7 +287,14 @@ const Tools = () => {
}
]}
>
-
+ {
+ input?.focus()
+ }}
+ />
>
diff --git a/src/pages/User/index.tsx b/src/pages/User/index.tsx
index 7af2d92..61ae9c6 100644
--- a/src/pages/User/index.tsx
+++ b/src/pages/User/index.tsx
@@ -11,6 +11,11 @@ import {
import { utcToLocalTime } from '@/util/datetime'
import { getUserInfo, removeToken } from '@/util/auth'
import { r_sys_user_info_change_password, r_sys_user_info_update } from '@/services/system'
+import {
+ r_auth_two_factor_create,
+ r_auth_two_factor_remove,
+ r_auth_two_factor_validate
+} from '@/services/auth'
import { r_api_avatar_random_base64 } from '@/services/api/avatar'
import FitFullscreen from '@/components/common/FitFullscreen'
import Card from '@/components/common/Card'
@@ -25,6 +30,7 @@ const User = () => {
const [modal, contextHolder] = AntdModal.useModal()
const [form] = AntdForm.useForm()
const formValues = AntdForm.useWatch([], form)
+ const [twoFactorForm] = AntdForm.useForm<{ twoFactorCode: string }>()
const [isLoading, setIsLoading] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmittable, setIsSubmittable] = useState(false)
@@ -93,6 +99,8 @@ const User = () => {
修改密码
>
),
+ getContainer: false,
+ centered: true,
maskClosable: true,
content: (
{
labelAlign={'right'}
rules={[{ required: true, message: '请输入原密码' }]}
>
-
+ {
+ input?.focus()
+ }}
+ />
{
})
}
+ const handleOnChangeTwoFactor = (enable: boolean) => {
+ return () => {
+ twoFactorForm.resetFields()
+ if (enable) {
+ void modal.confirm({
+ title: '双因素',
+ centered: true,
+ maskClosable: true,
+ content: '确定解除双因素?',
+ onOk: () => {
+ void modal.confirm({
+ title: '解除双因素',
+ getContainer: false,
+ centered: true,
+ maskClosable: true,
+ content: (
+ <>
+
+
+ {
+ input?.focus()
+ }}
+ />
+
+
+ >
+ ),
+ onOk: () =>
+ twoFactorForm.validateFields().then(
+ () => {
+ return new Promise((resolve) => {
+ void r_auth_two_factor_remove({
+ code: twoFactorForm.getFieldValue(
+ 'twoFactorCode'
+ ) as string
+ })
+ .then((res) => {
+ const response = res.data
+ if (response.success) {
+ void message.success('解绑成功')
+ getProfile()
+ } else {
+ void message.error('解绑失败,请稍后重试')
+ }
+ })
+ .finally(() => {
+ resolve()
+ })
+ })
+ },
+ () => {
+ return new Promise((_, reject) => {
+ reject('输入有误')
+ })
+ }
+ )
+ })
+ }
+ })
+ } else {
+ if (isLoading) {
+ return
+ }
+ setIsLoading(true)
+ void message.loading({ content: '加载中', key: 'LOADING', duration: 0 })
+ void r_auth_two_factor_create()
+ .then((res) => {
+ message.destroy('LOADING')
+ const response = res.data
+ if (response.success) {
+ void modal.confirm({
+ title: '绑定双因素',
+ getContainer: false,
+ centered: true,
+ maskClosable: true,
+ content: (
+ <>
+
+
+
+ {
+ input?.focus()
+ }}
+ />
+
+
+ >
+ ),
+ onOk: () =>
+ twoFactorForm.validateFields().then(
+ () => {
+ return new Promise((resolve) => {
+ void r_auth_two_factor_validate({
+ code: twoFactorForm.getFieldValue(
+ 'twoFactorCode'
+ ) as string
+ })
+ .then((res) => {
+ const response = res.data
+ if (response.success) {
+ void message.success('绑定成功')
+ getProfile()
+ } else {
+ void message.error(
+ '绑定失败,请稍后重试'
+ )
+ }
+ })
+ .finally(() => {
+ resolve()
+ })
+ })
+ },
+ () => {
+ return new Promise((_, reject) => {
+ reject('输入有误')
+ })
+ }
+ )
+ })
+ } else {
+ void message.error('获取双因素绑定二维码失败,请稍后重试')
+ }
+ })
+ .catch(() => {
+ message.destroy('LOADING')
+ })
+ .finally(() => {
+ setIsLoading(false)
+ })
+ }
+ }
+ }
+
const getProfile = () => {
if (isLoading) {
return
@@ -382,6 +549,40 @@ const User = () => {
+
+ 双因素
+
+
diff --git a/src/services/auth.tsx b/src/services/auth.tsx
index c470526..f3f080b 100644
--- a/src/services/auth.tsx
+++ b/src/services/auth.tsx
@@ -5,6 +5,7 @@ import {
URL_REGISTER,
URL_RESEND,
URL_RETRIEVE,
+ URL_TWO_FACTOR,
URL_VERIFY
} from '@/constants/urls.constants'
import request from '@/services'
@@ -21,4 +22,12 @@ export const r_auth_retrieve = (param: RetrieveParam) => request.post(URL_RETRIE
export const r_auth_login = (param: LoginParam) => request.post(URL_LOGIN, param)
+export const r_auth_two_factor_create = () => request.get(URL_TWO_FACTOR)
+
+export const r_auth_two_factor_validate = (param: TwoFactorValidateParam) =>
+ request.post(URL_TWO_FACTOR, param)
+
+export const r_auth_two_factor_remove = (param: TwoFactorRemoveParam) =>
+ request.delete(URL_TWO_FACTOR, param)
+
export const r_auth_logout = () => request.post(URL_LOGOUT)
diff --git a/src/services/system.tsx b/src/services/system.tsx
index 0047416..c64bef0 100644
--- a/src/services/system.tsx
+++ b/src/services/system.tsx
@@ -20,7 +20,8 @@ import {
URL_SYS_TOOL_CATEGORY,
URL_SYS_TOOL_BASE,
URL_SYS_TOOL_TEMPLATE,
- URL_SYS_TOOL
+ URL_SYS_TOOL,
+ URL_SYS_SETTINGS_TWO_FACTOR
} from '@/constants/urls.constants'
import request from '@/services/index'
@@ -111,6 +112,12 @@ export const r_sys_settings_sensitive_update = (param: SensitiveWordUpdateParam)
export const r_sys_settings_sensitive_delete = (id: string) =>
request.delete(`${URL_SYS_SETTINGS_SENSITIVE}/${id}`)
+export const r_sys_settings_two_factor_get = () =>
+ request.get(URL_SYS_SETTINGS_TWO_FACTOR)
+
+export const r_sys_settings_two_factor_update = (param: TwoFactorSettingsParam) =>
+ request.put(URL_SYS_SETTINGS_TWO_FACTOR, param)
+
export const r_sys_statistics_software = () =>
request.get(URL_SYS_STATISTICS_SOFTWARE)