Add two-factor
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user