Add two-factor

This commit is contained in:
2024-03-01 15:33:30 +08:00
parent e563c2e8be
commit 935a1a223a
12 changed files with 399 additions and 4 deletions

View File

@@ -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>
)
}

View 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

View File

@@ -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>

View File

@@ -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>