Add two-factor #38
1
src/assets/svg/lock.svg
Normal file
1
src/assets/svg/lock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M512 725.333333c46.933333 0 85.333333-38.4 85.333333-85.333333s-38.4-85.333333-85.333333-85.333333-85.333333 38.4-85.333333 85.333333 38.4 85.333333 85.333333 85.333333z m256-384h-42.666667v-85.333333c0-117.76-95.573333-213.333333-213.333333-213.333333S298.666667 138.24 298.666667 256v85.333333h-42.666667c-46.933333 0-85.333333 38.4-85.333333 85.333334v426.666666c0 46.933333 38.4 85.333333 85.333333 85.333334h512c46.933333 0 85.333333-38.4 85.333333-85.333334V426.666667c0-46.933333-38.4-85.333333-85.333333-85.333334z m-388.266667-85.333333c0-72.96 59.306667-132.266667 132.266667-132.266667s132.266667 59.306667 132.266667 132.266667v85.333333H379.733333v-85.333333zM768 853.333333H256V426.666667h512v426.666666z" /></svg>
|
||||||
|
After Width: | Height: | Size: 802 B |
1
src/assets/svg/safe.svg
Normal file
1
src/assets/svg/safe.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M512 78.869333l21.162667 12.096c69.717333 39.829333 150.677333 65.344 215.466666 80.874667 32.149333 7.722667 59.776 12.864 79.274667 16.064a824.981333 824.981333 0 0 0 28.437333 4.16l1.386667 0.170667 0.32 0.021333L896 196.48v361.728l-0.576 3.477333L853.333333 554.666667c42.090667 7.018667 42.069333 7.04 42.069334 7.061333v0.064l-0.021334 0.106667-0.064 0.32-0.170666 0.917333-0.554667 2.922667c-0.490667 2.410667-1.216 5.76-2.218667 9.898666-2.026667 8.277333-5.226667 19.861333-10.005333 33.856a495.445333 495.445333 0 0 1-51.797333 106.666667c-52.736 82.112-146.069333 174.869333-306.773334 221.098667l-11.797333 3.413333-11.797333-3.413333c-160.704-46.229333-254.037333-138.986667-306.773334-221.12a495.573333 495.573333 0 0 1-51.797333-106.645334 394.709333 394.709333 0 0 1-10.005333-33.856 262.250667 262.250667 0 0 1-2.773334-12.8l-0.192-0.938666-0.042666-0.32-0.021334-0.106667v-0.064c0-0.021333 0-0.042667 42.069334-7.061333l-42.090667 7.018666L128 558.208V196.48l37.952-4.224 0.32-0.021333 1.386667-0.170667 5.824-0.768c5.184-0.704 12.864-1.813333 22.613333-3.413333 19.498667-3.2 47.146667-8.32 79.274667-16.042667 64.768-15.530667 145.749333-41.045333 215.466666-80.874667L512 78.869333z m-298.666667 192.661334v278.976a310.4 310.4 0 0 0 9.045334 31.701333c7.765333 22.72 21.056 54.186667 42.858666 88.149333 42.026667 65.408 116.16 141.013333 246.762667 181.696 130.602667-40.661333 204.757333-116.288 246.762667-181.696a410.304 410.304 0 0 0 42.88-88.149333A309.76 309.76 0 0 0 810.666667 550.506667V271.530667c-20.906667-3.477333-49.28-8.853333-81.962667-16.704C666.154667 239.786667 586.026667 215.338667 512 176.618667c-74.026667 38.72-154.176 63.189333-216.704 78.208-32.704 7.850667-61.077333 13.226667-81.962667 16.704z m517.098667 146.154666L486.933333 661.184 328.746667 503.04l60.352-60.330667 97.834666 97.813334 183.146667-183.146667 60.352 60.330667z" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/svg/unlock.svg
Normal file
1
src/assets/svg/unlock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M512 725.333333c47.146667 0 85.333333-38.186667 85.333333-85.333333s-38.186667-85.333333-85.333333-85.333333-85.333333 38.186667-85.333333 85.333333 38.186667 85.333333 85.333333 85.333333z m256-384h-42.666667v-85.333333c0-117.76-95.573333-213.333333-213.333333-213.333333S298.666667 138.24 298.666667 256h81.066666c0-72.96 59.306667-132.266667 132.266667-132.266667 72.96 0 132.266667 59.306667 132.266667 132.266667v85.333333H256c-47.146667 0-85.333333 38.186667-85.333333 85.333334v426.666666c0 47.146667 38.186667 85.333333 85.333333 85.333334h512c47.146667 0 85.333333-38.186667 85.333333-85.333334V426.666667c0-47.146667-38.186667-85.333333-85.333333-85.333334z m0 512H256V426.666667h512v426.666666z" /></svg>
|
||||||
|
After Width: | Height: | Size: 788 B |
@@ -61,6 +61,10 @@ export const PERMISSION_ACCOUNT_NEED_INIT = 20064
|
|||||||
export const PERMISSION_USER_NOT_FOUND = 20065
|
export const PERMISSION_USER_NOT_FOUND = 20065
|
||||||
export const PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED = 20066
|
export const PERMISSION_RETRIEVE_CODE_ERROR_OR_EXPIRED = 20066
|
||||||
export const PERMISSION_ACCOUNT_NEED_RESET_PASSWORD = 20067
|
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_SUCCESS = 30000
|
||||||
export const DATABASE_SELECT_FAILED = 30005
|
export const DATABASE_SELECT_FAILED = 30005
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const URL_RETRIEVE = '/retrieve'
|
|||||||
export const URL_LOGIN = '/login'
|
export const URL_LOGIN = '/login'
|
||||||
export const URL_TOKEN = '/token'
|
export const URL_TOKEN = '/token'
|
||||||
export const URL_LOGOUT = '/logout'
|
export const URL_LOGOUT = '/logout'
|
||||||
|
export const URL_TWO_FACTOR = '/two-factor'
|
||||||
export const URL_SYS_LOG = '/system/log'
|
export const URL_SYS_LOG = '/system/log'
|
||||||
export const URL_SYS_USER_INFO = '/system/user/info'
|
export const URL_SYS_USER_INFO = '/system/user/info'
|
||||||
export const URL_SYS_USER = '/system/user'
|
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_BASE = `${URL_SYS_SETTINGS}/base`
|
||||||
export const URL_SYS_SETTINGS_MAIL = `${URL_SYS_SETTINGS}/mail`
|
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_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 = '/system/statistics'
|
||||||
export const URL_SYS_STATISTICS_SOFTWARE = `${URL_SYS_STATISTICS}/software`
|
export const URL_SYS_STATISTICS_SOFTWARE = `${URL_SYS_STATISTICS}/software`
|
||||||
export const URL_SYS_STATISTICS_HARDWARE = `${URL_SYS_STATISTICS}/hardware`
|
export const URL_SYS_STATISTICS_HARDWARE = `${URL_SYS_STATISTICS}/hardware`
|
||||||
|
|||||||
26
src/global.d.ts
vendored
26
src/global.d.ts
vendored
@@ -92,6 +92,7 @@ interface LoginParam {
|
|||||||
account: string
|
account: string
|
||||||
password: string
|
password: string
|
||||||
captchaCode: string
|
captchaCode: string
|
||||||
|
twoFactorCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserChangePasswordParam {
|
interface UserChangePasswordParam {
|
||||||
@@ -99,9 +100,18 @@ interface UserChangePasswordParam {
|
|||||||
newPassword: string
|
newPassword: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TwoFactorValidateParam {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TwoFactorRemoveParam {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
interface UserWithInfoVo {
|
interface UserWithInfoVo {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
|
twoFactor: boolean
|
||||||
verified: boolean
|
verified: boolean
|
||||||
locking: boolean
|
locking: boolean
|
||||||
expiration: string
|
expiration: string
|
||||||
@@ -119,6 +129,7 @@ interface UserWithInfoVo {
|
|||||||
interface UserWithPowerInfoVo {
|
interface UserWithPowerInfoVo {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
|
twoFactor: boolean
|
||||||
verified: boolean
|
verified: boolean
|
||||||
locking: boolean
|
locking: boolean
|
||||||
expiration: string
|
expiration: string
|
||||||
@@ -140,6 +151,7 @@ interface UserWithPowerInfoVo {
|
|||||||
interface UserWithRoleInfoVo {
|
interface UserWithRoleInfoVo {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
|
twoFactor: boolean
|
||||||
verify: string
|
verify: string
|
||||||
locking: boolean
|
locking: boolean
|
||||||
expiration: string
|
expiration: string
|
||||||
@@ -164,6 +176,10 @@ interface UserInfoVo {
|
|||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TwoFactorVo {
|
||||||
|
qrCodeSVGBase64: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ModuleVo {
|
interface ModuleVo {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -402,6 +418,16 @@ interface SensitiveWordUpdateParam {
|
|||||||
ids: string[]
|
ids: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TwoFactorSettingsVo {
|
||||||
|
issuer: string
|
||||||
|
secretKeyLength: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TwoFactorSettingsParam {
|
||||||
|
issuer: string
|
||||||
|
secretKeyLength: number
|
||||||
|
}
|
||||||
|
|
||||||
interface SoftwareInfoVo {
|
interface SoftwareInfoVo {
|
||||||
os: string
|
os: string
|
||||||
bitness: number
|
bitness: number
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
H_CAPTCHA_SITE_KEY,
|
H_CAPTCHA_SITE_KEY,
|
||||||
PERMISSION_LOGIN_SUCCESS,
|
PERMISSION_LOGIN_SUCCESS,
|
||||||
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
|
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
|
||||||
|
PERMISSION_NEED_TWO_FACTOR,
|
||||||
|
PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR,
|
||||||
PERMISSION_USER_DISABLE,
|
PERMISSION_USER_DISABLE,
|
||||||
PERMISSION_USERNAME_NOT_FOUND,
|
PERMISSION_USERNAME_NOT_FOUND,
|
||||||
SYSTEM_INVALID_CAPTCHA_CODE
|
SYSTEM_INVALID_CAPTCHA_CODE
|
||||||
@@ -16,6 +18,7 @@ import FitCenter from '@/components/common/FitCenter'
|
|||||||
import FlexBox from '@/components/common/FlexBox'
|
import FlexBox from '@/components/common/FlexBox'
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
|
const [modal, contextHolder] = AntdModal.useModal()
|
||||||
const { refreshRouter } = useContext(AppContext)
|
const { refreshRouter } = useContext(AppContext)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
@@ -29,6 +32,7 @@ const SignIn = () => {
|
|||||||
},
|
},
|
||||||
[location.pathname]
|
[location.pathname]
|
||||||
)
|
)
|
||||||
|
const [twoFactorForm] = AntdForm.useForm<{ twoFactorCode: string }>()
|
||||||
const [isSigningIn, setIsSigningIn] = useState(false)
|
const [isSigningIn, setIsSigningIn] = useState(false)
|
||||||
const [captchaCode, setCaptchaCode] = useState('')
|
const [captchaCode, setCaptchaCode] = useState('')
|
||||||
|
|
||||||
@@ -55,7 +59,8 @@ const SignIn = () => {
|
|||||||
void r_auth_login({
|
void r_auth_login({
|
||||||
account: loginParam.account,
|
account: loginParam.account,
|
||||||
password: loginParam.password,
|
password: loginParam.password,
|
||||||
captchaCode
|
captchaCode,
|
||||||
|
twoFactorCode: loginParam.twoFactorCode
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const response = res.data
|
const response = res.data
|
||||||
@@ -96,6 +101,56 @@ const SignIn = () => {
|
|||||||
})
|
})
|
||||||
}, 1500)
|
}, 1500)
|
||||||
break
|
break
|
||||||
|
case PERMISSION_NEED_TWO_FACTOR:
|
||||||
|
twoFactorForm.resetFields()
|
||||||
|
void modal.confirm({
|
||||||
|
title: '双因素验证',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<AntdForm form={twoFactorForm}>
|
||||||
|
<AntdForm.Item
|
||||||
|
name={'twoFactorCode'}
|
||||||
|
label={'验证码'}
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
rules={[{ required: true, len: 6 }]}
|
||||||
|
>
|
||||||
|
<AntdInput
|
||||||
|
showCount
|
||||||
|
maxLength={6}
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AntdForm.Item>
|
||||||
|
</AntdForm>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk: () =>
|
||||||
|
twoFactorForm.validateFields().then(
|
||||||
|
() => {
|
||||||
|
return new Promise<void>((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_USERNAME_NOT_FOUND:
|
||||||
case PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR:
|
case PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR:
|
||||||
void message.error(
|
void message.error(
|
||||||
@@ -105,6 +160,10 @@ const SignIn = () => {
|
|||||||
)
|
)
|
||||||
setIsSigningIn(false)
|
setIsSigningIn(false)
|
||||||
break
|
break
|
||||||
|
case PERMISSION_TWO_FACTOR_VERIFICATION_CODE_ERROR:
|
||||||
|
void message.error('双因素验证码错误')
|
||||||
|
setIsSigningIn(false)
|
||||||
|
break
|
||||||
case PERMISSION_USER_DISABLE:
|
case PERMISSION_USER_DISABLE:
|
||||||
void message.error(
|
void message.error(
|
||||||
<>
|
<>
|
||||||
@@ -170,7 +229,13 @@ const SignIn = () => {
|
|||||||
/>
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
<FlexBox direction={'horizontal'} className={'addition'}>
|
<FlexBox direction={'horizontal'} className={'addition'}>
|
||||||
<AntdCheckbox disabled={isSigningIn}>记住密码</AntdCheckbox>
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
返回首页
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/forget${location.search}`, { replace: true })
|
navigate(`/forget${location.search}`, { replace: true })
|
||||||
@@ -203,6 +268,7 @@ const SignIn = () => {
|
|||||||
</AntdForm>
|
</AntdForm>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
</FitCenter>
|
</FitCenter>
|
||||||
|
{contextHolder}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const Base = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
icon={IconOxygenEmail}
|
icon={IconOxygenBase}
|
||||||
title={'基础'}
|
title={'基础'}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onReset={handleOnReset}
|
onReset={handleOnReset}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const Mail = () => {
|
|||||||
const handleOnTest = () => {
|
const handleOnTest = () => {
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '发送测试邮件',
|
title: '发送测试邮件',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
@@ -27,7 +29,11 @@ const Mail = () => {
|
|||||||
style={{ marginTop: 10 }}
|
style={{ marginTop: 10 }}
|
||||||
rules={[{ required: true, type: 'email' }]}
|
rules={[{ required: true, type: 'email' }]}
|
||||||
>
|
>
|
||||||
<AntdInput />
|
<AntdInput
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
</AntdForm>
|
</AntdForm>
|
||||||
<AntdTag style={{ whiteSpace: 'normal' }}>
|
<AntdTag style={{ whiteSpace: 'normal' }}>
|
||||||
@@ -56,7 +62,7 @@ const Mail = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
return new Promise((_, reject) => {
|
return new Promise((_, reject) => {
|
||||||
reject('未输入接收者')
|
reject('输入有误')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
73
src/pages/System/Settings/TwoFactor.tsx
Normal file
73
src/pages/System/Settings/TwoFactor.tsx
Normal file
@@ -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<TwoFactorSettingsParam>()
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<SettingsCard
|
||||||
|
icon={IconOxygenSafe}
|
||||||
|
title={'双因素'}
|
||||||
|
loading={loading}
|
||||||
|
onReset={handleOnReset}
|
||||||
|
onSave={handleOnSave}
|
||||||
|
modifyOperationCode={['system:settings:modify:two-factor']}
|
||||||
|
>
|
||||||
|
<AntdForm
|
||||||
|
form={twoFactorForm}
|
||||||
|
labelCol={{ flex: '7em' }}
|
||||||
|
disabled={!hasPermission('system:settings:modify:two-factor')}
|
||||||
|
>
|
||||||
|
<AntdForm.Item label={'提供者'} name={'issuer'}>
|
||||||
|
<AntdInput />
|
||||||
|
</AntdForm.Item>
|
||||||
|
<AntdForm.Item label={'密钥长度'} name={'secretKeyLength'}>
|
||||||
|
<AntdInputNumber min={3} max={64} style={{ width: '100%' }} />
|
||||||
|
</AntdForm.Item>
|
||||||
|
</AntdForm>
|
||||||
|
</SettingsCard>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TwoFactor
|
||||||
@@ -10,6 +10,7 @@ import Permission from '@/components/common/Permission'
|
|||||||
import Base from '@/pages/System/Settings/Base'
|
import Base from '@/pages/System/Settings/Base'
|
||||||
import Mail from '@/pages/System/Settings/Mail'
|
import Mail from '@/pages/System/Settings/Mail'
|
||||||
import SensitiveWord from '@/pages/System/Settings/SensitiveWord'
|
import SensitiveWord from '@/pages/System/Settings/SensitiveWord'
|
||||||
|
import TwoFactor from '@/pages/System/Settings/TwoFactor.tsx'
|
||||||
|
|
||||||
interface SettingsCardProps extends PropsWithChildren {
|
interface SettingsCardProps extends PropsWithChildren {
|
||||||
icon: IconComponent
|
icon: IconComponent
|
||||||
@@ -68,6 +69,9 @@ const Settings = () => {
|
|||||||
<Permission operationCode={['system:settings:query:mail']}>
|
<Permission operationCode={['system:settings:query:mail']}>
|
||||||
<Mail />
|
<Mail />
|
||||||
</Permission>
|
</Permission>
|
||||||
|
<Permission operationCode={['system:settings:query:two-factor']}>
|
||||||
|
<TwoFactor />
|
||||||
|
</Permission>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
</HideScrollbar>
|
</HideScrollbar>
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ const Base = () => {
|
|||||||
compileForm.setFieldValue('entryFileName', undefined)
|
compileForm.setFieldValue('entryFileName', undefined)
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '编译',
|
title: '编译',
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
@@ -537,6 +538,8 @@ const Base = () => {
|
|||||||
const handleOnAddFile = () => {
|
const handleOnAddFile = () => {
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '新建文件',
|
title: '新建文件',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm form={addFileForm}>
|
<AntdForm form={addFileForm}>
|
||||||
@@ -565,7 +568,11 @@ const Base = () => {
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<AntdInput />
|
<AntdInput
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
</AntdForm>
|
</AntdForm>
|
||||||
),
|
),
|
||||||
@@ -714,6 +721,8 @@ const Base = () => {
|
|||||||
renameFileForm.setFieldValue('fileName', fileName)
|
renameFileForm.setFieldValue('fileName', fileName)
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '重命名文件',
|
title: '重命名文件',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm form={renameFileForm}>
|
<AntdForm form={renameFileForm}>
|
||||||
@@ -745,7 +754,11 @@ const Base = () => {
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<AntdInput />
|
<AntdInput
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
</AntdForm>
|
</AntdForm>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -399,6 +399,8 @@ const Template = () => {
|
|||||||
const handleOnAddFile = () => {
|
const handleOnAddFile = () => {
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '新建文件',
|
title: '新建文件',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm form={addFileForm}>
|
<AntdForm form={addFileForm}>
|
||||||
@@ -427,7 +429,11 @@ const Template = () => {
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<AntdInput />
|
<AntdInput
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
</AntdForm>
|
</AntdForm>
|
||||||
),
|
),
|
||||||
@@ -579,6 +585,8 @@ const Template = () => {
|
|||||||
renameFileForm.setFieldValue('fileName', fileName)
|
renameFileForm.setFieldValue('fileName', fileName)
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '重命名文件',
|
title: '重命名文件',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm form={renameFileForm}>
|
<AntdForm form={renameFileForm}>
|
||||||
@@ -610,7 +618,11 @@ const Template = () => {
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<AntdInput />
|
<AntdInput
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
</AntdForm>
|
</AntdForm>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ const Tools = () => {
|
|||||||
form.setFieldValue('pass', undefined)
|
form.setFieldValue('pass', undefined)
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '审核',
|
title: '审核',
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm form={form}>
|
<AntdForm form={form}>
|
||||||
|
|||||||
@@ -336,6 +336,8 @@ const User = () => {
|
|||||||
修改用户 {value.username} 的密码
|
修改用户 {value.username} 的密码
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm
|
<AntdForm
|
||||||
@@ -356,7 +358,11 @@ const User = () => {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<AntdInput.Password />
|
<AntdInput.Password
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
<AntdForm.Item
|
<AntdForm.Item
|
||||||
name={'passwordConfirm'}
|
name={'passwordConfirm'}
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ const Tools = () => {
|
|||||||
const handleOnUpgradeTool = (tool: ToolVo) => {
|
const handleOnUpgradeTool = (tool: ToolVo) => {
|
||||||
void modal.confirm({
|
void modal.confirm({
|
||||||
title: '更新工具',
|
title: '更新工具',
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
@@ -285,7 +287,14 @@ const Tools = () => {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<AntdInput maxLength={10} showCount placeholder={'请输入版本'} />
|
<AntdInput
|
||||||
|
maxLength={10}
|
||||||
|
showCount
|
||||||
|
placeholder={'请输入版本'}
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
</AntdForm>
|
</AntdForm>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ import {
|
|||||||
import { utcToLocalTime } from '@/util/datetime'
|
import { utcToLocalTime } from '@/util/datetime'
|
||||||
import { getUserInfo, removeToken } from '@/util/auth'
|
import { getUserInfo, removeToken } from '@/util/auth'
|
||||||
import { r_sys_user_info_change_password, r_sys_user_info_update } from '@/services/system'
|
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 { r_api_avatar_random_base64 } from '@/services/api/avatar'
|
||||||
import FitFullscreen from '@/components/common/FitFullscreen'
|
import FitFullscreen from '@/components/common/FitFullscreen'
|
||||||
import Card from '@/components/common/Card'
|
import Card from '@/components/common/Card'
|
||||||
@@ -25,6 +30,7 @@ const User = () => {
|
|||||||
const [modal, contextHolder] = AntdModal.useModal()
|
const [modal, contextHolder] = AntdModal.useModal()
|
||||||
const [form] = AntdForm.useForm<UserInfoUpdateParam>()
|
const [form] = AntdForm.useForm<UserInfoUpdateParam>()
|
||||||
const formValues = AntdForm.useWatch([], form)
|
const formValues = AntdForm.useWatch([], form)
|
||||||
|
const [twoFactorForm] = AntdForm.useForm<{ twoFactorCode: string }>()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [isSubmittable, setIsSubmittable] = useState(false)
|
const [isSubmittable, setIsSubmittable] = useState(false)
|
||||||
@@ -93,6 +99,8 @@ const User = () => {
|
|||||||
修改密码
|
修改密码
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
getContainer: false,
|
||||||
|
centered: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
content: (
|
content: (
|
||||||
<AntdForm
|
<AntdForm
|
||||||
@@ -107,7 +115,12 @@ const User = () => {
|
|||||||
labelAlign={'right'}
|
labelAlign={'right'}
|
||||||
rules={[{ required: true, message: '请输入原密码' }]}
|
rules={[{ required: true, message: '请输入原密码' }]}
|
||||||
>
|
>
|
||||||
<AntdInput.Password placeholder={'请输入原密码'} />
|
<AntdInput.Password
|
||||||
|
placeholder={'请输入原密码'}
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
<AntdForm.Item
|
<AntdForm.Item
|
||||||
name={'newPassword'}
|
name={'newPassword'}
|
||||||
@@ -196,6 +209,160 @@ const User = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: (
|
||||||
|
<>
|
||||||
|
<AntdForm form={twoFactorForm}>
|
||||||
|
<AntdForm.Item
|
||||||
|
name={'twoFactorCode'}
|
||||||
|
label={'验证码'}
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
rules={[{ required: true, len: 6 }]}
|
||||||
|
>
|
||||||
|
<AntdInput
|
||||||
|
showCount
|
||||||
|
maxLength={6}
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AntdForm.Item>
|
||||||
|
</AntdForm>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk: () =>
|
||||||
|
twoFactorForm.validateFields().then(
|
||||||
|
() => {
|
||||||
|
return new Promise<void>((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: (
|
||||||
|
<>
|
||||||
|
<AntdImage
|
||||||
|
src={`data:image/svg+xml;base64,${response.data?.qrCodeSVGBase64}`}
|
||||||
|
alt={'Two-factor'}
|
||||||
|
preview={false}
|
||||||
|
/>
|
||||||
|
<AntdForm form={twoFactorForm}>
|
||||||
|
<AntdForm.Item
|
||||||
|
name={'twoFactorCode'}
|
||||||
|
label={'验证码'}
|
||||||
|
style={{ marginTop: 10, marginRight: 30 }}
|
||||||
|
rules={[{ required: true, len: 6 }]}
|
||||||
|
>
|
||||||
|
<AntdInput
|
||||||
|
showCount
|
||||||
|
maxLength={6}
|
||||||
|
ref={(input) => {
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AntdForm.Item>
|
||||||
|
</AntdForm>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk: () =>
|
||||||
|
twoFactorForm.validateFields().then(
|
||||||
|
() => {
|
||||||
|
return new Promise<void>((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 = () => {
|
const getProfile = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return
|
return
|
||||||
@@ -382,6 +549,40 @@ const User = () => {
|
|||||||
</AntdSpace.Compact>
|
</AntdSpace.Compact>
|
||||||
</div>
|
</div>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
|
<FlexBox className={'row'} direction={'horizontal'}>
|
||||||
|
<div className={'label'}>双因素</div>
|
||||||
|
<div className={'input'}>
|
||||||
|
<AntdSpace.Compact>
|
||||||
|
<AntdInput
|
||||||
|
disabled
|
||||||
|
style={{
|
||||||
|
color: userWithPowerInfoVo?.twoFactor
|
||||||
|
? COLOR_PRODUCTION
|
||||||
|
: undefined
|
||||||
|
}}
|
||||||
|
value={
|
||||||
|
userWithPowerInfoVo?.twoFactor ? '已设置' : '未设置'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AntdButton
|
||||||
|
type={'primary'}
|
||||||
|
title={userWithPowerInfoVo?.twoFactor ? '解绑' : '绑定'}
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={handleOnChangeTwoFactor(
|
||||||
|
userWithPowerInfoVo?.twoFactor ?? false
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
component={
|
||||||
|
userWithPowerInfoVo?.twoFactor
|
||||||
|
? IconOxygenUnlock
|
||||||
|
: IconOxygenLock
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AntdButton>
|
||||||
|
</AntdSpace.Compact>
|
||||||
|
</div>
|
||||||
|
</FlexBox>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
</Card>
|
</Card>
|
||||||
</HideScrollbar>
|
</HideScrollbar>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
URL_REGISTER,
|
URL_REGISTER,
|
||||||
URL_RESEND,
|
URL_RESEND,
|
||||||
URL_RETRIEVE,
|
URL_RETRIEVE,
|
||||||
|
URL_TWO_FACTOR,
|
||||||
URL_VERIFY
|
URL_VERIFY
|
||||||
} from '@/constants/urls.constants'
|
} from '@/constants/urls.constants'
|
||||||
import request from '@/services'
|
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<TokenVo>(URL_LOGIN, param)
|
export const r_auth_login = (param: LoginParam) => request.post<TokenVo>(URL_LOGIN, param)
|
||||||
|
|
||||||
|
export const r_auth_two_factor_create = () => request.get<TwoFactorVo>(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)
|
export const r_auth_logout = () => request.post(URL_LOGOUT)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import {
|
|||||||
URL_SYS_TOOL_CATEGORY,
|
URL_SYS_TOOL_CATEGORY,
|
||||||
URL_SYS_TOOL_BASE,
|
URL_SYS_TOOL_BASE,
|
||||||
URL_SYS_TOOL_TEMPLATE,
|
URL_SYS_TOOL_TEMPLATE,
|
||||||
URL_SYS_TOOL
|
URL_SYS_TOOL,
|
||||||
|
URL_SYS_SETTINGS_TWO_FACTOR
|
||||||
} from '@/constants/urls.constants'
|
} from '@/constants/urls.constants'
|
||||||
import request from '@/services/index'
|
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) =>
|
export const r_sys_settings_sensitive_delete = (id: string) =>
|
||||||
request.delete(`${URL_SYS_SETTINGS_SENSITIVE}/${id}`)
|
request.delete(`${URL_SYS_SETTINGS_SENSITIVE}/${id}`)
|
||||||
|
|
||||||
|
export const r_sys_settings_two_factor_get = () =>
|
||||||
|
request.get<TwoFactorSettingsVo>(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 = () =>
|
export const r_sys_statistics_software = () =>
|
||||||
request.get<SoftwareInfoVo>(URL_SYS_STATISTICS_SOFTWARE)
|
request.get<SoftwareInfoVo>(URL_SYS_STATISTICS_SOFTWARE)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user