mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-10-17 04:57:15 +08:00
feat: 系统配置新增安全设置功能
1、在系统配置中增加安全配置,支持配置密码策略 2、移除个人安全设置 3、在账号管理中增加修改密码功能 4、每次登录后检测密码是否过期并提示修改
This commit is contained in:
@@ -17,17 +17,44 @@
|
||||
<div class="sub-text">
|
||||
密码至少包含
|
||||
<span class="sub-text-value">大写字母</span>
|
||||
<span class="sub-text-value">大写字母</span>
|
||||
<span class="sub-text-value">小写字母</span>
|
||||
<span class="sub-text-value">数字</span>
|
||||
<span class="sub-text-value">特殊字符</span>3种
|
||||
<span class="sub-text-value" v-if="securityConfig.password_special_char.value == 1">特殊字符</span>
|
||||
</div>
|
||||
<div class="sub-text" v-if="securityConfig.password_contain_name.value == 1">
|
||||
密码不能包含<span class="sub-text-value">正反序用户名</span>
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
密码长度至少
|
||||
<span class="sub-text-value">
|
||||
{{ securityConfig.password_min_length.value }}
|
||||
</span>
|
||||
位
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
<div v-if="securityConfig.password_expiration_days.value == 0">未设置密码有效期</div>
|
||||
<div v-else>
|
||||
密码有效期
|
||||
<span class="sub-text-value">
|
||||
{{ securityConfig.password_expiration_days.value }}
|
||||
</span>
|
||||
天
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
连续密码错误可重试
|
||||
<span class="sub-text-value">
|
||||
{{ securityConfig.password_error_count.value }}
|
||||
</span>
|
||||
次
|
||||
</div>
|
||||
<div class="sub-text">
|
||||
超过错误密码重试次数账号将被锁定
|
||||
<span class="sub-text-value">
|
||||
{{ securityConfig.password_lock_minutes.value }}
|
||||
</span>
|
||||
分钟
|
||||
</div>
|
||||
<div class="sub-text">限制密码长度至少为<span class="sub-text-value">6</span>位</div>
|
||||
<div class="sub-text">未设置密码有效期</div>
|
||||
<div class="sub-text">新密码不能与历史前<span class="sub-text-value">N</span>次密码重复</div>
|
||||
<div class="sub-text">1小时内密码错误可重试<span class="sub-text-value">N</span>次</div>
|
||||
<div class="sub-text">超过错误密码重试次数账号将被锁定<span class="sub-text-value">N</span>分钟</div>
|
||||
<a-link class="link">修改规则(未开放)</a-link>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
@@ -38,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { updateUserPassword } from '@/apis'
|
||||
import { listOption, type OptionResp, type SecurityConfigResp, updateUserPassword } from '@/apis'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { encryptByRsa } from '@/utils/encrypt'
|
||||
import { type Columns, GiForm } from '@/components/GiForm'
|
||||
@@ -60,7 +87,7 @@ const columns: Columns = [
|
||||
label: '当前密码',
|
||||
field: 'oldPassword',
|
||||
type: 'input-password',
|
||||
rules: [{ required: true, message: '请输入当前密码' }],
|
||||
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }],
|
||||
hide: () => {
|
||||
return userInfo.pwdResetTime
|
||||
}
|
||||
@@ -69,13 +96,13 @@ const columns: Columns = [
|
||||
label: '新密码',
|
||||
field: 'newPassword',
|
||||
type: 'input-password',
|
||||
rules: [{ required: true, message: '请输入新密码' }]
|
||||
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }]
|
||||
},
|
||||
{
|
||||
label: '确认新密码',
|
||||
field: 'rePassword',
|
||||
type: 'input-password',
|
||||
rules: [{ required: true, message: '请再次输入新密码' }],
|
||||
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }],
|
||||
props: {
|
||||
placeholder: '请再次输入新密码'
|
||||
}
|
||||
@@ -105,6 +132,14 @@ const onUpdate = async () => {
|
||||
const save = async () => {
|
||||
const isInvalid = await formRef.value?.formRef?.validate()
|
||||
if (isInvalid) return false
|
||||
if (form.newPassword !== form.rePassword) {
|
||||
Message.error('两次新密码不一致')
|
||||
return false
|
||||
}
|
||||
if (form.newPassword === form.oldPassword) {
|
||||
Message.error('新密码与旧密码不能相同')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await updateUserPassword({
|
||||
oldPassword: encryptByRsa(form.oldPassword) || '',
|
||||
@@ -116,6 +151,28 @@ const save = async () => {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const securityConfig = ref<SecurityConfigResp>({
|
||||
password_contain_name: {},
|
||||
password_error_count: {},
|
||||
password_expiration_days: {},
|
||||
password_lock_minutes: {},
|
||||
password_min_length: {},
|
||||
password_special_char: {},
|
||||
password_update_interval: {}
|
||||
})
|
||||
|
||||
// 查询列表数据
|
||||
const getDataList = async () => {
|
||||
const { data } = await listOption({ code: Object.keys(securityConfig.value) })
|
||||
securityConfig.value = data.reduce((obj: SecurityConfigResp, option: OptionResp) => {
|
||||
obj[option.code] = { ...option, value: parseInt(option.value) }
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<div class="gi_page">
|
||||
<a-row wrap :gutter="16">
|
||||
<a-row wrap :gutter="16" align="stretch">
|
||||
<a-col :xs="24" :sm="24" :md="10" :lg="10" :xl="7" :xxl="7">
|
||||
<LeftBox />
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :md="14" :lg="14" :xl="17" :xxl="17">
|
||||
<RightBox />
|
||||
<div>
|
||||
<PasswordPolicy />
|
||||
</div>
|
||||
<div style="margin-top: 16px">
|
||||
<RightBox />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
@@ -14,6 +19,7 @@
|
||||
<script setup lang="ts">
|
||||
import LeftBox from './LeftBox.vue'
|
||||
import RightBox from './RightBox.vue'
|
||||
import PasswordPolicy from './PasswordPolicy.vue'
|
||||
|
||||
defineOptions({ name: 'SettingProfile' })
|
||||
</script>
|
||||
|
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<a-card title="账号保护" bordered class="gradient-card">
|
||||
<div v-for="item in modeList" :key="item.title">
|
||||
<div class="item">
|
||||
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
|
||||
<div class="info">
|
||||
<div class="info-top">
|
||||
<span class="label">{{ item.title }}</span>
|
||||
<span class="bind">
|
||||
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
|
||||
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
|
||||
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{
|
||||
item.status ? '已开启' : '未开启'
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-desc">
|
||||
{{ item.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<a-switch disabled title="未开放" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ModeItem } from '../type'
|
||||
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
modeList.value = [
|
||||
{
|
||||
title: '登录保护',
|
||||
icon: 'protect',
|
||||
subtitle: '开启登录保护后,账号登录需进行二次身份验证',
|
||||
status: false
|
||||
},
|
||||
{
|
||||
title: '操作保护',
|
||||
icon: 'protect',
|
||||
subtitle: '进行敏感操作时需进行二次身份校验',
|
||||
status: false
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<a-card title="基本设置" bordered class="gradient-card">
|
||||
<div v-for="item in modeList" :key="item.title">
|
||||
<div class="item">
|
||||
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
|
||||
<div class="info">
|
||||
<div class="info-top">
|
||||
<span class="label">{{ item.title }}</span>
|
||||
<span class="bind">
|
||||
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
|
||||
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
|
||||
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{
|
||||
item.status ? '已绑定' : '未绑定'
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-desc">
|
||||
<span class="value">{{ item.value }}</span>
|
||||
{{ item.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<a-button class="btn" :type="item.status ? 'secondary' : 'primary'" @click="onUpdate(item.type)">
|
||||
{{ item.status ? '修改' : '绑定' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
<VerifyModel ref="verifyModelRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ModeItem } from '../type'
|
||||
import VerifyModel from '../components/VerifyModel.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
modeList.value = [
|
||||
{
|
||||
title: '安全手机',
|
||||
icon: 'phone-color',
|
||||
value: `${userInfo.value.phone + ' ' || '手机号'}`,
|
||||
subtitle: `可用于身份验证、密码找回、通知接收`,
|
||||
type: 'phone',
|
||||
status: !!userInfo.value.phone
|
||||
},
|
||||
{
|
||||
title: '安全邮箱',
|
||||
icon: 'email-color',
|
||||
value: `${userInfo.value.email + ' ' || '邮箱'}`,
|
||||
subtitle: `可用于身份验证、密码找回、通知接收`,
|
||||
type: 'email',
|
||||
status: !!userInfo.value.email
|
||||
}
|
||||
]
|
||||
|
||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
||||
// 修改
|
||||
const onUpdate = (type: string) => {
|
||||
verifyModelRef.value?.open(type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mode-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
.mode-item-content {
|
||||
display: flex;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<a-card title="登录会话设置" bordered class="gradient-card">
|
||||
<div class="item">
|
||||
<div class="icon-wrapper"><GiSvgIcon name="message-color" :size="26" /></div>
|
||||
<div class="info">
|
||||
<div class="info-top">
|
||||
<span class="label">登录态保持时间设置</span>
|
||||
</div>
|
||||
<div class="info-desc">保持登录状态的限制</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail">
|
||||
<div class="sub-text-wrapper">
|
||||
<div class="sub-text">无操作登录会话保持<span class="sub-text-value">30</span>分钟,超时登录会话将失效</div>
|
||||
<div class="sub-text">登录会话最大保持<span class="sub-text-value">0</span>天,超时登录会话将失效</div>
|
||||
<a-link class="link">修改规则(未开放)</a-link>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<div class="gi_page">
|
||||
<a-row wrap :gutter="16">
|
||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
||||
<BasicsSetting />
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
||||
<SessionSetting />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row wrap :gutter="16" style="margin-top: 16px">
|
||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
||||
<PasswordPolicy />
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" :xxl="12">
|
||||
<AccountProtection />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BasicsSetting from './BasicsSetting.vue'
|
||||
import SessionSetting from './SessionSetting.vue'
|
||||
import PasswordPolicy from './PasswordPolicy.vue'
|
||||
import AccountProtection from './AccountProtection.vue'
|
||||
|
||||
defineOptions({ name: 'SettingSecurity' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gi_page {
|
||||
background-color: var(--color-bg-1);
|
||||
.flex_box {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
.flex_item_container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
& .flex_item_container:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user