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_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
|
||||
|
||||
@@ -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`
|
||||
|
||||
26
src/global.d.ts
vendored
26
src/global.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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: (
|
||||
<>
|
||||
<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_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 = () => {
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
<FlexBox direction={'horizontal'} className={'addition'}>
|
||||
<AntdCheckbox disabled={isSigningIn}>记住密码</AntdCheckbox>
|
||||
<a
|
||||
onClick={() => {
|
||||
navigate('/')
|
||||
}}
|
||||
>
|
||||
返回首页
|
||||
</a>
|
||||
<a
|
||||
onClick={() => {
|
||||
navigate(`/forget${location.search}`, { replace: true })
|
||||
@@ -203,6 +268,7 @@ const SignIn = () => {
|
||||
</AntdForm>
|
||||
</FlexBox>
|
||||
</FitCenter>
|
||||
{contextHolder}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const Base = () => {
|
||||
return (
|
||||
<>
|
||||
<SettingsCard
|
||||
icon={IconOxygenEmail}
|
||||
icon={IconOxygenBase}
|
||||
title={'基础'}
|
||||
loading={loading}
|
||||
onReset={handleOnReset}
|
||||
|
||||
@@ -17,6 +17,8 @@ const Mail = () => {
|
||||
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' }]}
|
||||
>
|
||||
<AntdInput />
|
||||
<AntdInput
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
</AntdForm>
|
||||
<AntdTag style={{ whiteSpace: 'normal' }}>
|
||||
@@ -56,7 +62,7 @@ const Mail = () => {
|
||||
},
|
||||
() => {
|
||||
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 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 = () => {
|
||||
<Permission operationCode={['system:settings:query:mail']}>
|
||||
<Mail />
|
||||
</Permission>
|
||||
<Permission operationCode={['system:settings:query:two-factor']}>
|
||||
<TwoFactor />
|
||||
</Permission>
|
||||
</FlexBox>
|
||||
</FlexBox>
|
||||
</HideScrollbar>
|
||||
|
||||
@@ -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: (
|
||||
<AntdForm form={addFileForm}>
|
||||
@@ -565,7 +568,11 @@ const Base = () => {
|
||||
})
|
||||
]}
|
||||
>
|
||||
<AntdInput />
|
||||
<AntdInput
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
</AntdForm>
|
||||
),
|
||||
@@ -714,6 +721,8 @@ const Base = () => {
|
||||
renameFileForm.setFieldValue('fileName', fileName)
|
||||
void modal.confirm({
|
||||
title: '重命名文件',
|
||||
getContainer: false,
|
||||
centered: true,
|
||||
maskClosable: true,
|
||||
content: (
|
||||
<AntdForm form={renameFileForm}>
|
||||
@@ -745,7 +754,11 @@ const Base = () => {
|
||||
})
|
||||
]}
|
||||
>
|
||||
<AntdInput />
|
||||
<AntdInput
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
</AntdForm>
|
||||
),
|
||||
|
||||
@@ -399,6 +399,8 @@ const Template = () => {
|
||||
const handleOnAddFile = () => {
|
||||
void modal.confirm({
|
||||
title: '新建文件',
|
||||
getContainer: false,
|
||||
centered: true,
|
||||
maskClosable: true,
|
||||
content: (
|
||||
<AntdForm form={addFileForm}>
|
||||
@@ -427,7 +429,11 @@ const Template = () => {
|
||||
})
|
||||
]}
|
||||
>
|
||||
<AntdInput />
|
||||
<AntdInput
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
</AntdForm>
|
||||
),
|
||||
@@ -579,6 +585,8 @@ const Template = () => {
|
||||
renameFileForm.setFieldValue('fileName', fileName)
|
||||
void modal.confirm({
|
||||
title: '重命名文件',
|
||||
getContainer: false,
|
||||
centered: true,
|
||||
maskClosable: true,
|
||||
content: (
|
||||
<AntdForm form={renameFileForm}>
|
||||
@@ -610,7 +618,11 @@ const Template = () => {
|
||||
})
|
||||
]}
|
||||
>
|
||||
<AntdInput />
|
||||
<AntdInput
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
</AntdForm>
|
||||
),
|
||||
|
||||
@@ -195,6 +195,7 @@ const Tools = () => {
|
||||
form.setFieldValue('pass', undefined)
|
||||
void modal.confirm({
|
||||
title: '审核',
|
||||
centered: true,
|
||||
maskClosable: true,
|
||||
content: (
|
||||
<AntdForm form={form}>
|
||||
|
||||
@@ -336,6 +336,8 @@ const User = () => {
|
||||
修改用户 {value.username} 的密码
|
||||
</>
|
||||
),
|
||||
getContainer: false,
|
||||
centered: true,
|
||||
maskClosable: true,
|
||||
content: (
|
||||
<AntdForm
|
||||
@@ -356,7 +358,11 @@ const User = () => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<AntdInput.Password />
|
||||
<AntdInput.Password
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
<AntdForm.Item
|
||||
name={'passwordConfirm'}
|
||||
|
||||
@@ -262,6 +262,8 @@ const Tools = () => {
|
||||
const handleOnUpgradeTool = (tool: ToolVo) => {
|
||||
void modal.confirm({
|
||||
title: '更新工具',
|
||||
getContainer: false,
|
||||
centered: true,
|
||||
maskClosable: true,
|
||||
content: (
|
||||
<>
|
||||
@@ -285,7 +287,14 @@ const Tools = () => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<AntdInput maxLength={10} showCount placeholder={'请输入版本'} />
|
||||
<AntdInput
|
||||
maxLength={10}
|
||||
showCount
|
||||
placeholder={'请输入版本'}
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
</AntdForm>
|
||||
</>
|
||||
|
||||
@@ -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<UserInfoUpdateParam>()
|
||||
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: (
|
||||
<AntdForm
|
||||
@@ -107,7 +115,12 @@ const User = () => {
|
||||
labelAlign={'right'}
|
||||
rules={[{ required: true, message: '请输入原密码' }]}
|
||||
>
|
||||
<AntdInput.Password placeholder={'请输入原密码'} />
|
||||
<AntdInput.Password
|
||||
placeholder={'请输入原密码'}
|
||||
ref={(input) => {
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
<AntdForm.Item
|
||||
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 = () => {
|
||||
if (isLoading) {
|
||||
return
|
||||
@@ -382,6 +549,40 @@ const User = () => {
|
||||
</AntdSpace.Compact>
|
||||
</div>
|
||||
</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>
|
||||
</Card>
|
||||
</HideScrollbar>
|
||||
|
||||
@@ -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<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)
|
||||
|
||||
@@ -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<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 = () =>
|
||||
request.get<SoftwareInfoVo>(URL_SYS_STATISTICS_SOFTWARE)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user