feat: 分享功能密码增强close #8593

This commit is contained in:
fit2cloud-chenyw 2024-04-08 14:02:18 +08:00
parent 9732a99ace
commit 657bbf55e6
14 changed files with 406 additions and 84 deletions

View File

@ -5,11 +5,11 @@ import java.io.Serializable;
/**
* <p>
*
* 公共链接
* </p>
*
* @author fit2cloud
* @since 2023-09-22
* @since 2024-04-07
*/
@TableName("xpack_share")
public class XpackShare implements Serializable {
@ -61,6 +61,11 @@ public class XpackShare implements Serializable {
*/
private Integer type;
/**
* 自动生成密码
*/
private Boolean autoPwd;
public Long getId() {
return id;
}
@ -133,6 +138,14 @@ public class XpackShare implements Serializable {
this.type = type;
}
public Boolean getAutoPwd() {
return autoPwd;
}
public void setAutoPwd(Boolean autoPwd) {
this.autoPwd = autoPwd;
}
@Override
public String toString() {
return "XpackShare{" +
@ -145,6 +158,7 @@ public class XpackShare implements Serializable {
", resourceId = " + resourceId +
", oid = " + oid +
", type = " + type +
", autoPwd = " + autoPwd +
"}";
}
}

View File

@ -6,11 +6,11 @@ import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* 公共链接 Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2023-09-22
* @since 2024-04-07
*/
@Mapper
public interface XpackShareMapper extends BaseMapper<XpackShare> {

View File

@ -82,12 +82,13 @@ public class XpackShareManage {
xpackShareMapper.updateById(originData);
}
public void editPwd(Long resourceId, String pwd) {
public void editPwd(Long resourceId, String pwd, Boolean autoPwd) {
XpackShare originData = queryByResource(resourceId);
if (ObjectUtils.isEmpty(originData)) {
DEException.throwException("share instance not exist");
}
originData.setPwd(pwd);
originData.setAutoPwd(ObjectUtils.isEmpty(autoPwd) || autoPwd);
xpackShareMapper.updateById(originData);
}
@ -169,8 +170,7 @@ public class XpackShareManage {
if (StringUtils.isBlank(xpackShare.getPwd())) return true;
if (StringUtils.isBlank(ciphertext)) return false;
String text = RsaUtils.decryptStr(ciphertext);
int len = text.length();
int splitIndex = len - 4;
int splitIndex = 8;
String pwd = text.substring(splitIndex);
String uuid = text.substring(0, splitIndex);
return StringUtils.equals(xpackShare.getUuid(), uuid) && StringUtils.equals(xpackShare.getPwd(), pwd);
@ -178,8 +178,7 @@ public class XpackShareManage {
public boolean validatePwd(XpackSharePwdValidator validator) {
String ciphertext = RsaUtils.decryptStr(validator.getCiphertext());
int len = ciphertext.length();
int splitIndex = len - 4;
int splitIndex = 8;
String pwd = ciphertext.substring(splitIndex);
String uuid = ciphertext.substring(0, splitIndex);
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();

View File

@ -44,7 +44,7 @@ public class XpackShareServer implements XpackShareApi {
@Override
public void editPwd(XpackSharePwdRequest request) {
xpackShareManage.editPwd(request.getResourceId(), request.getPwd());
xpackShareManage.editPwd(request.getResourceId(), request.getPwd(), request.getAutoPwd());
}
@Override

View File

@ -0,0 +1,2 @@
ALTER TABLE `xpack_share`
ADD COLUMN `auto_pwd` tinyint(1) NOT NULL DEFAULT 1 COMMENT '自动生成密码' AFTER `type`;

View File

@ -0,0 +1,13 @@
export const vClickOutside = {
beforeMount(el, binding) {
el.clickOutsideEvent = function (event) {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideEvent)
}
}

View File

@ -1,5 +1,7 @@
import { checkPermission } from './Permission'
import { vClickOutside } from './ClickOutside'
import type { App } from 'vue'
export const installDirective = (app: App<Element>) => {
app.directive('permission', checkPermission)
app.directive('click-outside', vClickOutside)
}

View File

@ -1884,6 +1884,7 @@ export default {
copy_short_link: '复制短链接',
copy_short_link_passwd: '复制短链接及密码',
passwd_protect: '密码保护',
auto_pwd: '自动生成密码',
link: '链接',
over_time: '有效期',
link_expire: '链接已过期',

View File

@ -15,7 +15,8 @@
<el-form-item label="" prop="password">
<CustomPassword
v-model="form.password"
maxlength="4"
maxlength="10"
minlength="4"
show-password
class="real-input"
:placeholder="t('pblink.input_placeholder')"
@ -64,7 +65,7 @@ const rule = reactive<FormRules>({
{ required: true, message: t('pblink.key_pwd'), trigger: 'blur' },
{
required: true,
pattern: /^[a-zA-Z0-9]{4}$/,
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/,
message: t('pblink.pwd_format_error'),
trigger: 'blur'
}

View File

@ -20,6 +20,7 @@
v-model="dialogVisible"
:close-on-click-modal="true"
:append-to-body="true"
:before-close="beforeClose"
title="公共链接分享"
width="480px"
:show-close="false"
@ -62,12 +63,34 @@
@change="pwdEnableSwitcher"
:label="t('visualization.passwd_protect')"
/>
<div class="inline-share-item" v-if="state.detailInfo.pwd">
<el-input v-model="state.detailInfo.pwd" readonly size="small">
<div class="auto-pwd-container" v-if="passwdEnable">
<el-checkbox
:disabled="!shareEnable"
v-model="state.detailInfo.autoPwd"
@change="autoEnableSwitcher"
:label="t('visualization.auto_pwd')"
/>
</div>
<div class="inline-share-item" v-if="passwdEnable">
<el-input
ref="pwdRef"
v-model="state.detailInfo.pwd"
:readonly="state.detailInfo.autoPwd"
size="small"
@blur="validatePwdFormat"
>
<template #append>
<div @click.stop="resetPwd" class="share-reset-container">
<span>{{ t('commons.reset') }}</span>
<div class="share-pwd-opt">
<div
v-if="state.detailInfo.autoPwd"
@click.stop="resetPwd"
class="share-reset-container"
>
<span>{{ t('commons.reset') }}</span>
</div>
<div @click.stop="copyPwd" class="share-reset-container">
<span>{{ t('commons.copy') }}</span>
</div>
</div>
</template>
</el-input>
@ -87,7 +110,7 @@
<script lang="ts" setup>
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import request from '@/config/axios'
import { propTypes } from '@/utils/propTypes'
import { ShareInfo, SHARE_BASE, shortcuts } from './option'
@ -102,6 +125,7 @@ const props = defineProps({
weight: propTypes.number.def(0),
isButton: propTypes.bool.def(false)
})
const pwdRef = ref(null)
const loadingInstance = ref<any>(null)
const dialogVisible = ref(false)
const overTimeEnable = ref(false)
@ -114,7 +138,8 @@ const state = reactive({
id: '',
uuid: '',
pwd: '',
exp: 0
exp: 0,
autoPwd: true
} as ShareInfo
})
const emits = defineEmits(['loaded'])
@ -122,7 +147,22 @@ const shareTips = computed(
() =>
`开启后,用户可以通过该链接访问${props.resourceType === 'dashboard' ? '仪表板' : '数据大屏'}`
)
const copyPwd = async () => {
if (shareEnable.value && passwdEnable.value) {
if (!state.detailInfo.autoPwd && existErrorMsg()) {
ElMessage.warning('密码格式错误,请重新填写!')
return
}
try {
await toClipboard(state.detailInfo.pwd)
ElMessage.success(t('common.copy_success'))
} catch (e) {
ElMessage.warning(t('common.copy_unsupported'))
}
} else {
ElMessage.warning(t('common.copy_unsupported'))
}
}
const copyInfo = async () => {
if (shareEnable.value) {
try {
@ -220,22 +260,88 @@ const expChangeHandler = exp => {
loadShareInfo()
})
}
const beforeClose = done => {
if (validatePwdFormat()) {
done()
}
}
const validatePwdFormat = () => {
if (!shareEnable.value || state.detailInfo.autoPwd) {
showPageError(null)
return true
}
const val = state.detailInfo.pwd
if (!val) {
showPageError('密码不能为空,请重新输入!')
return false
}
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/
if (!regex.test(val)) {
showPageError('密码必须是包含数字、字母、特殊字符[!@#$%^&*()_+]的4-10位字符串')
return false
}
showPageError(null)
resetPwdHandler(val, false)
return true
}
const showPageError = msg => {
const domRef = pwdRef
if (!domRef.value) {
return
}
const e = domRef.value.input
if (!msg) {
e.style = null
e.style.borderColor = null
const child = e.parentElement.querySelector('.link-pwd-error-msg')
if (child) {
e.parentElement['style'] = null
e.parentElement.removeChild(child)
}
} else {
e.style.color = 'red'
e.style.borderColor = 'red'
e.parentElement['style']['box-shadow'] = '0 0 0 1px red inset'
const child = e.parentElement.querySelector('.link-pwd-error-msg')
if (!child) {
const errorDom = document.createElement('div')
errorDom.className = 'link-pwd-error-msg'
errorDom.innerText = msg
e.parentElement.appendChild(errorDom)
} else {
child.innerText = msg
}
}
}
const existErrorMsg = () => {
return document.getElementsByClassName('link-pwd-error-msg')?.length
}
const autoEnableSwitcher = val => {
if (val) {
showPageError(null)
resetPwd()
} else {
state.detailInfo.pwd = ''
nextTick(() => {
pwdRef.value.input.focus()
})
}
}
const pwdEnableSwitcher = val => {
let pwd = ''
if (val) {
pwd = getUuid()
}
resetPwdHandler(pwd)
resetPwdHandler(pwd, true)
}
const resetPwd = () => {
const pwd = getUuid()
resetPwdHandler(pwd)
resetPwdHandler(pwd, true)
}
const resetPwdHandler = (pwd?: string) => {
const resetPwdHandler = (pwd?: string, autoPwd?: boolean) => {
const resourceId = props.resourceId
const url = '/share/editPwd'
const data = { resourceId, pwd }
const data = { resourceId, pwd, autoPwd }
request.post({ url, data }).then(() => {
loadShareInfo()
})
@ -314,6 +420,9 @@ onMounted(() => {
}
}
.pwd-container {
.auto-pwd-container {
padding: 0 25px 6px;
}
.ed-checkbox {
margin-right: 10px;
}
@ -322,24 +431,41 @@ onMounted(() => {
width: 220px;
:deep(.ed-input-group__append) {
width: 45px !important;
width: initial !important;
background: none;
color: #1f2329;
padding: 0px 0px !important;
.share-reset-container {
width: 100%;
.share-pwd-opt {
display: flex;
justify-content: center;
}
&:hover {
cursor: pointer;
background-color: #f5f6f7;
}
&:active {
cursor: pointer;
background-color: #eff0f1;
padding: 1px;
.share-reset-container {
&:not(:first-child) {
border-left: 1px solid var(--ed-input-border-color) !important;
}
width: 45px;
display: flex;
justify-content: center;
&:hover {
cursor: pointer;
background-color: #f5f6f7;
}
&:active {
cursor: pointer;
background-color: #eff0f1;
}
}
}
}
:deep(.link-pwd-error-msg) {
color: red;
position: absolute;
z-index: 9;
font-size: 10px;
height: 10px;
top: 21px;
width: 350px;
left: 0px;
}
}
}

View File

@ -1,22 +1,26 @@
<template>
<el-button secondary ref="shareButtonRef" v-if="props.weight >= 7" v-click-outside="openShare">
<template #icon>
<icon name="icon_share-label_outlined"></icon>
</template>
{{ t('visualization.share') }}
</el-button>
<el-popover
ref="sharePopoverRef"
:virtual-ref="shareButtonRef"
trigger="click"
:visible="popoverVisible"
title=""
virtual-triggering
width="480"
placement="bottom-start"
:show-arrow="false"
popper-class="share-popover"
@show="share"
>
<template #reference>
<el-button
secondary
v-if="props.weight >= 7"
@click="openPopover"
v-click-outside="clickOutPopover"
>
<template #icon>
<icon name="icon_share-label_outlined"></icon>
</template>
{{ t('visualization.share') }}
</el-button>
</template>
<div class="share-container">
<div class="share-title share-padding">公共链接分享</div>
<div class="open-share flex-align-center share-padding">
@ -58,11 +62,34 @@
@change="pwdEnableSwitcher"
:label="t('visualization.passwd_protect')"
/>
<div class="inline-share-item" v-if="state.detailInfo.pwd">
<el-input v-model="state.detailInfo.pwd" readonly size="small">
<div class="auto-pwd-container" v-if="passwdEnable">
<el-checkbox
:disabled="!shareEnable"
v-model="state.detailInfo.autoPwd"
@change="autoEnableSwitcher"
:label="t('visualization.auto_pwd')"
/>
</div>
<div class="inline-share-item" v-if="passwdEnable">
<el-input
ref="pwdRef"
v-model="state.detailInfo.pwd"
:readonly="state.detailInfo.autoPwd"
size="small"
@blur="validatePwdFormat"
>
<template #append>
<div @click="resetPwd" class="share-reset-container">
<span>{{ t('commons.reset') }}</span>
<div class="share-pwd-opt">
<div
v-if="state.detailInfo.autoPwd"
@click.stop="resetPwd"
class="share-reset-container"
>
<span>{{ t('commons.reset') }}</span>
</div>
<div @click.stop="copyPwd" class="share-reset-container">
<span>{{ t('commons.copy') }}</span>
</div>
</div>
</template>
</el-input>
@ -81,7 +108,7 @@
<script lang="ts" setup>
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive, unref, computed } from 'vue'
import { ref, reactive, computed, nextTick, watch } from 'vue'
import request from '@/config/axios'
import { propTypes } from '@/utils/propTypes'
import { ShareInfo, SHARE_BASE, shortcuts } from './option'
@ -94,11 +121,9 @@ const props = defineProps({
resourceType: propTypes.string.def(''),
weight: propTypes.number.def(0)
})
const shareButtonRef = ref()
const sharePopoverRef = ref()
const popoverVisible = ref(false)
const pwdRef = ref(null)
const loadingInstance = ref<any>(null)
const dialogVisible = ref(false)
const overTimeEnable = ref(false)
const passwdEnable = ref(false)
const shareEnable = ref(false)
@ -109,14 +134,34 @@ const state = reactive({
id: '',
uuid: '',
pwd: '',
exp: 0
exp: 0,
autoPwd: true
} as ShareInfo
})
const openShare = () => {
unref(sharePopoverRef).popperRef?.delayHide?.()
watch(
() => props.resourceId,
() => {
popoverVisible.value = false
}
)
const hideShare = () => {
if (validatePwdFormat()) {
popoverVisible.value = false
return
}
}
const clickOutPopover = e => {
if (!popoverVisible.value || e.target.closest('[class*="share-popover"]')) {
return
}
hideShare()
}
const openPopover = () => {
if (!popoverVisible.value) {
popoverVisible.value = true
}
}
const shareTips = computed(
() =>
`开启后,用户可以通过该链接访问${props.resourceType === 'dashboard' ? '仪表板' : '数据大屏'}`
@ -133,8 +178,7 @@ const copyInfo = async () => {
} else {
ElMessage.warning(t('common.copy_unsupported'))
}
dialogVisible.value = false
openShare()
hideShare()
}
const disabledDate = date => {
@ -149,7 +193,6 @@ const closeLoading = () => {
}
const share = () => {
dialogVisible.value = true
loadShareInfo()
}
@ -230,27 +273,125 @@ const pwdEnableSwitcher = val => {
if (val) {
pwd = getUuid()
}
resetPwdHandler(pwd)
resetPwdHandler(pwd, true)
}
const resetPwd = () => {
const pwd = getUuid()
resetPwdHandler(pwd)
resetPwdHandler(pwd, true)
}
const resetPwdHandler = (pwd?: string) => {
const resetPwdHandler = (pwd?: string, autoPwd?: boolean) => {
const resourceId = props.resourceId
const url = '/share/editPwd'
const data = { resourceId, pwd }
const data = { resourceId, pwd, autoPwd }
request.post({ url, data }).then(() => {
loadShareInfo()
})
}
const getUuid = () => {
return 'xyxy'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
const length = 10
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+'
let result = ''
const specialChars = '!@#$%^&*()_+'
let hasSpecialChar = false
for (let i = 0; i < length; i++) {
if (i === 0) {
result += characters.charAt(Math.floor(Math.random() * characters.length))
} else {
if (!hasSpecialChar && i < length - 2) {
result += specialChars.charAt(Math.floor(Math.random() * specialChars.length))
hasSpecialChar = true
} else {
result += characters.charAt(Math.floor(Math.random() * characters.length))
}
}
}
result = result
.split('')
.sort(() => 0.5 - Math.random())
.join('')
return result
}
const validatePwdFormat = () => {
if (!shareEnable.value || !passwdEnable.value || state.detailInfo.autoPwd) {
showPageError(null)
return true
}
const val = state.detailInfo.pwd
if (!val) {
showPageError('密码不能为空,请重新输入!')
return false
}
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/
if (!regex.test(val)) {
showPageError('密码必须是包含数字、字母、特殊字符[!@#$%^&*()_+]的4-10位字符串')
return false
}
showPageError(null)
resetPwdHandler(val, false)
return true
}
const showPageError = msg => {
const domRef = pwdRef
if (!domRef.value) {
return
}
const e = domRef.value.input
if (!msg) {
e.style = null
e.style.borderColor = null
const child = e.parentElement.querySelector('.link-pwd-error-msg')
if (child) {
e.parentElement['style'] = null
e.parentElement.removeChild(child)
}
} else {
e.style.color = 'red'
e.style.borderColor = 'red'
e.parentElement['style']['box-shadow'] = '0 0 0 1px red inset'
const child = e.parentElement.querySelector('.link-pwd-error-msg')
if (!child) {
const errorDom = document.createElement('div')
errorDom.className = 'link-pwd-error-msg'
errorDom.innerText = msg
e.parentElement.appendChild(errorDom)
} else {
child.innerText = msg
}
}
}
const existErrorMsg = () => {
return document.getElementsByClassName('link-pwd-error-msg')?.length
}
const autoEnableSwitcher = val => {
if (val) {
showPageError(null)
resetPwd()
} else {
state.detailInfo.pwd = ''
nextTick(() => {
pwdRef.value.input.focus()
})
}
}
const copyPwd = async () => {
if (shareEnable.value && passwdEnable.value) {
if (!state.detailInfo.autoPwd && existErrorMsg()) {
ElMessage.warning('密码格式错误,请重新填写!')
return
}
try {
await toClipboard(state.detailInfo.pwd)
ElMessage.success(t('common.copy_success'))
} catch (e) {
ElMessage.warning(t('common.copy_unsupported'))
}
} else {
ElMessage.warning(t('common.copy_unsupported'))
}
}
const execute = () => {
@ -315,23 +456,41 @@ defineExpose({
width: 220px;
:deep(.ed-input-group__append) {
width: 45px !important;
width: initial !important;
background: none;
color: #1f2329;
padding: 0px 0px !important;
.share-reset-container {
width: 100%;
.share-pwd-opt {
display: flex;
justify-content: center;
}
&:hover {
cursor: pointer;
background-color: #f5f6f7;
}
&:active {
cursor: pointer;
background-color: #eff0f1;
padding: 1px;
.share-reset-container {
&:not(:first-child) {
border-left: 1px solid var(--ed-input-border-color) !important;
}
width: 45px;
display: flex;
justify-content: center;
&:hover {
cursor: pointer;
background-color: #f5f6f7;
}
&:active {
cursor: pointer;
background-color: #eff0f1;
}
}
}
}
:deep(.link-pwd-error-msg) {
color: red;
position: absolute;
z-index: 9;
font-size: 10px;
height: 10px;
top: 21px;
width: 350px;
left: 0px;
}
}
</style>

View File

@ -3,6 +3,7 @@ export interface ShareInfo {
exp?: number
uuid: string
pwd?: string
autoPwd: boolean
}
export const SHARE_BASE = '/de-link/'

View File

@ -20,4 +20,6 @@ public class XpackSharePwdRequest implements Serializable {
@Schema(description = "密码")
private String pwd;
@Schema(description = "自动生成密码")
private Boolean autoPwd = true;
}

View File

@ -27,4 +27,6 @@ public class XpackShareVO implements Serializable {
private String uuid;
@Schema(description = "分享密码")
private String pwd;
@Schema(description = "自动生成密码")
private Boolean autoPwd = true;
}