Merge pull request #9000 from dataease/pr@dev-v2@feat_share_uuid_custom

feat: 公共链接后缀可自定义close #8195
This commit is contained in:
fit2cloud-chenyw 2024-04-08 18:23:38 +08:00 committed by GitHub
commit 4066c3bcee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 306 additions and 71 deletions

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.dataease.api.visualization.request.VisualizationWorkbranchQueryRequest;
import io.dataease.api.xpack.share.request.XpackShareProxyRequest;
import io.dataease.api.xpack.share.request.XpackSharePwdValidator;
import io.dataease.api.xpack.share.request.XpackShareUuidEditor;
import io.dataease.api.xpack.share.vo.XpackShareGridVO;
import io.dataease.api.xpack.share.vo.XpackShareProxyVO;
import io.dataease.auth.bo.TokenUserBO;
@ -31,6 +32,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Component("xpackShareManage")
@ -70,6 +73,35 @@ public class XpackShareManage {
xpackShareMapper.insert(xpackShare);
}
public String editUuid(XpackShareUuidEditor editor) {
Long resourceId = editor.getResourceId();
String uuid = editor.getUuid();
XpackShare originData = queryByResource(resourceId);
if (ObjectUtils.isEmpty(originData)) {
return "公共链接不存在,请先创建!";
}
if (StringUtils.isBlank(uuid)) {
return "不能为空!";
}
if (StringUtils.equals(uuid, originData.getUuid())) {
return "";
}
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("uuid", uuid);
if (xpackShareMapper.selectCount(queryWrapper) > 0) {
return "已存在相同的链接,请重新输入!";
}
String regex = "^[a-zA-Z0-9]{8,16}$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(uuid);
if (!matcher.matches()) {
return "仅支持8-16位(字母数字),请重新输入!";
}
originData.setUuid(uuid);
xpackShareMapper.updateById(originData);
return "";
}
public void editExp(Long resourceId, Long exp) {
XpackShare originData = queryByResource(resourceId);
if (ObjectUtils.isEmpty(originData)) {
@ -92,6 +124,8 @@ public class XpackShareManage {
xpackShareMapper.updateById(originData);
}
public IPage<XpackSharePO> querySharePage(int goPage, int pageSize, VisualizationWorkbranchQueryRequest request) {
Long uid = AuthUtils.getUser().getUserId();
QueryWrapper<Object> queryWrapper = new QueryWrapper<>();
@ -170,16 +204,16 @@ public class XpackShareManage {
if (StringUtils.isBlank(xpackShare.getPwd())) return true;
if (StringUtils.isBlank(ciphertext)) return false;
String text = RsaUtils.decryptStr(ciphertext);
int splitIndex = 8;
String pwd = text.substring(splitIndex);
int splitIndex = text.indexOf(",");
String pwd = text.substring(splitIndex + 1);
String uuid = text.substring(0, splitIndex);
return StringUtils.equals(xpackShare.getUuid(), uuid) && StringUtils.equals(xpackShare.getPwd(), pwd);
}
public boolean validatePwd(XpackSharePwdValidator validator) {
String ciphertext = RsaUtils.decryptStr(validator.getCiphertext());
int splitIndex = 8;
String pwd = ciphertext.substring(splitIndex);
int splitIndex = ciphertext.indexOf(",");
String pwd = ciphertext.substring(splitIndex + 1);
String uuid = ciphertext.substring(0, splitIndex);
QueryWrapper<XpackShare> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("uuid", uuid);

View File

@ -2,10 +2,7 @@ package io.dataease.share.server;
import io.dataease.api.visualization.request.VisualizationWorkbranchQueryRequest;
import io.dataease.api.xpack.share.XpackShareApi;
import io.dataease.api.xpack.share.request.XpackShareExpRequest;
import io.dataease.api.xpack.share.request.XpackShareProxyRequest;
import io.dataease.api.xpack.share.request.XpackSharePwdRequest;
import io.dataease.api.xpack.share.request.XpackSharePwdValidator;
import io.dataease.api.xpack.share.request.*;
import io.dataease.api.xpack.share.vo.XpackShareGridVO;
import io.dataease.api.xpack.share.vo.XpackShareProxyVO;
import io.dataease.api.xpack.share.vo.XpackShareVO;
@ -70,7 +67,12 @@ public class XpackShareServer implements XpackShareApi {
}
@Override
public Map<String, String> queryRelationByUserId(@PathVariable("uid") Long uid) {
public Map<String, String> queryRelationByUserId(Long uid) {
return xpackShareManage.queryRelationByUserId(uid);
}
@Override
public String editUuid(XpackShareUuidEditor editor) {
return xpackShareManage.editUuid(editor);
}
}

View File

@ -50,6 +50,8 @@ import { rsaEncryp } from '@/utils/encryption'
import { useCache } from '@/hooks/web/useCache'
import { queryDekey } from '@/api/login'
import { CustomPassword } from '@/components/custom-password'
import { useRoute } from 'vue-router'
const route = useRoute()
const { wsCache } = useCache()
const appStore = useAppStoreWithOut()
@ -76,15 +78,9 @@ const refresh = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const curLocation = window.location.href
const paramIndex = curLocation.indexOf('?')
const uuidIndex = curLocation.indexOf('de-link/') + 8
const uuid = curLocation.substring(
uuidIndex,
paramIndex !== -1 ? paramIndex : curLocation.length
)
const uuid = route.params.uuid
const pwd = form.value.password
const text = uuid + pwd
const text = `${uuid},${pwd}`
const ciphertext = rsaEncryp(text)
request.post({ url: '/share/validate', data: { ciphertext } }).then(res => {
if (res.data) {
@ -100,6 +96,7 @@ const refresh = async (formEl: FormInstance | undefined) => {
})
}
onMounted(() => {
debugger
if (!wsCache.get(appStore.getDekey)) {
queryDekey()
.then(res => {

View File

@ -31,7 +31,26 @@
<el-switch size="small" v-model="shareEnable" @change="enableSwitcher" />
{{ shareTips }}
</div>
<div v-if="shareEnable" class="text">{{ linkAddr }}</div>
<div v-if="shareEnable" class="custom-link-line">
<el-input
ref="linkUuidRef"
placeholder=""
v-model="state.detailInfo.uuid"
:disabled="!linkCustom"
@blur="finishEditUuid"
>
<template #prefix>
{{ formatLinkBase() }}
</template>
</el-input>
<el-button v-if="linkCustom" text @click="finishEditUuid">完成</el-button>
<el-button v-else @click="editUuid" size="default" plain>
<template #icon>
<icon name="icon_admin_outlined"></icon>
</template>
</el-button>
</div>
<div v-if="shareEnable" class="exp-container">
<el-checkbox
:disabled="!shareEnable"
@ -133,6 +152,8 @@ const passwdEnable = ref(false)
const shareEnable = ref(false)
const linkAddr = ref('')
const expError = ref(false)
const linkCustom = ref(false)
const linkUuidRef = ref(null)
const state = reactive({
detailInfo: {
id: '',
@ -147,9 +168,46 @@ const shareTips = computed(
() =>
`开启后,用户可以通过该链接访问${props.resourceType === 'dashboard' ? '仪表板' : '数据大屏'}`
)
const editUuid = () => {
linkCustom.value = true
nextTick(() => {
if (linkUuidRef?.value) {
linkUuidRef.value.input.focus()
}
})
}
const validateUuid = async () => {
const val = state.detailInfo.uuid
const className = 'link-uuid-error-msg'
if (!val) {
showPageError('不能为空!', linkUuidRef, className)
return false
}
const regex = /^[a-zA-Z0-9]{8,16}$/
const result = regex.test(val)
if (!result) {
showPageError('仅支持8-16位(字母数字),请重新输入!', linkUuidRef, className)
} else {
const msg = await uuidValidateApi(val)
showPageError(msg, linkUuidRef, className)
return !msg
}
return result
}
const uuidValidateApi = async val => {
const url = '/share/editUuid'
const data = { resourceId: props.resourceId, uuid: val }
const res = await request.post({ url, data })
return res.data
}
const finishEditUuid = async () => {
const uuidValid = await validateUuid()
linkCustom.value = !uuidValid
}
const copyPwd = async () => {
if (shareEnable.value && passwdEnable.value) {
if (!state.detailInfo.autoPwd && existErrorMsg()) {
if (!state.detailInfo.autoPwd && existErrorMsg('link-pwd-error-msg')) {
ElMessage.warning('密码格式错误,请重新填写!')
return
}
@ -166,6 +224,10 @@ const copyPwd = async () => {
const copyInfo = async () => {
if (shareEnable.value) {
try {
if (existErrorMsg('link-uuid-error-msg')) {
ElMessage.warning('链接格式错误,请重新填写!')
return
}
await toClipboard(linkAddr.value)
ElMessage.success(t('common.copy_success'))
} catch (e) {
@ -226,6 +288,9 @@ const enableSwitcher = () => {
}
const formatLinkAddr = () => {
linkAddr.value = formatLinkBase() + state.detailInfo.uuid
}
const formatLinkBase = () => {
let prefix = '/'
if (window.DataEaseBi?.baseUrl) {
prefix = window.DataEaseBi.baseUrl + '#'
@ -233,7 +298,7 @@ const formatLinkAddr = () => {
const href = window.location.href
prefix = href.substring(0, href.indexOf('#') + 1)
}
linkAddr.value = prefix + SHARE_BASE + state.detailInfo.uuid
return prefix + SHARE_BASE
}
const expEnableSwitcher = val => {
@ -260,32 +325,36 @@ const expChangeHandler = exp => {
loadShareInfo()
})
}
const beforeClose = done => {
if (validatePwdFormat()) {
const beforeClose = async done => {
const pwdValid = validatePwdFormat()
const uuidValid = await validateUuid()
if (pwdValid && uuidValid) {
done()
}
}
const validatePwdFormat = () => {
if (!shareEnable.value || state.detailInfo.autoPwd) {
showPageError(null)
showPageError(null, pwdRef)
return true
}
const val = state.detailInfo.pwd
if (!val) {
showPageError('密码不能为空,请重新输入!')
showPageError('密码不能为空,请重新输入!', pwdRef)
return false
}
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/
if (!regex.test(val)) {
showPageError('密码必须是包含数字、字母、特殊字符[!@#$%^&*()_+]的4-10位字符串')
showPageError('密码必须是包含数字、字母、特殊字符[!@#$%^&*()_+]的4-10位字符串', pwdRef)
return false
}
showPageError(null)
showPageError(null, pwdRef)
resetPwdHandler(val, false)
return true
}
const showPageError = msg => {
const domRef = pwdRef
const showPageError = (msg, target, className?: string) => {
className = className || 'link-pwd-error-msg'
const fullClassName = `.${className}`
const domRef = target || pwdRef
if (!domRef.value) {
return
}
@ -293,7 +362,7 @@ const showPageError = msg => {
if (!msg) {
e.style = null
e.style.borderColor = null
const child = e.parentElement.querySelector('.link-pwd-error-msg')
const child = e.parentElement.querySelector(fullClassName)
if (child) {
e.parentElement['style'] = null
e.parentElement.removeChild(child)
@ -302,10 +371,10 @@ const showPageError = msg => {
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')
const child = e.parentElement.querySelector(fullClassName)
if (!child) {
const errorDom = document.createElement('div')
errorDom.className = 'link-pwd-error-msg'
errorDom.className = className
errorDom.innerText = msg
e.parentElement.appendChild(errorDom)
} else {
@ -313,12 +382,12 @@ const showPageError = msg => {
}
}
}
const existErrorMsg = () => {
return document.getElementsByClassName('link-pwd-error-msg')?.length
const existErrorMsg = (className: string) => {
return document.getElementsByClassName(className)?.length
}
const autoEnableSwitcher = val => {
if (val) {
showPageError(null)
showPageError(null, pwdRef)
resetPwd()
} else {
state.detailInfo.pwd = ''
@ -348,11 +417,29 @@ const resetPwdHandler = (pwd?: string, autoPwd?: boolean) => {
}
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 execute = () => {
@ -483,18 +570,26 @@ onMounted(() => {
margin-right: 8px;
}
}
.text {
border-radius: 4px;
border: 1px solid #bbbfc4;
background: #eff0f1;
.custom-link-line {
display: flex;
margin-bottom: 16px;
height: 32px;
padding: 5px 12px;
color: #8f959e;
font-size: 14px;
font-style: normal;
line-height: 22px;
align-items: center;
button {
width: 40px;
min-width: 40px;
margin-left: 8px;
height: 100%;
}
:deep(.link-uuid-error-msg) {
color: red;
position: absolute;
z-index: 9;
font-size: 10px;
height: 10px;
top: 25px;
width: 350px;
left: 0px;
}
}
}
}

View File

@ -27,8 +27,27 @@
<el-switch size="small" v-model="shareEnable" @change="enableSwitcher" />
{{ shareTips }}
</div>
<div v-if="shareEnable" class="text share-padding">
<!-- <div v-if="shareEnable" class="text share-padding">
<el-input v-model="linkAddr" disabled />
</div> -->
<div v-if="shareEnable" class="custom-link-line share-padding">
<el-input
ref="linkUuidRef"
placeholder=""
v-model="state.detailInfo.uuid"
:disabled="!linkCustom"
@blur="finishEditUuid"
>
<template #prefix>
{{ formatLinkBase() }}
</template>
</el-input>
<el-button v-if="linkCustom" text @click.stop="finishEditUuid">完成</el-button>
<el-button v-else @click.stop="editUuid" size="default" plain>
<template #icon>
<icon name="icon_admin_outlined"></icon>
</template>
</el-button>
</div>
<div v-if="shareEnable" class="exp-container share-padding">
<el-checkbox
@ -129,6 +148,8 @@ const passwdEnable = ref(false)
const shareEnable = ref(false)
const linkAddr = ref('')
const expError = ref(false)
const linkCustom = ref(false)
const linkUuidRef = ref(null)
const state = reactive({
detailInfo: {
id: '',
@ -145,8 +166,10 @@ watch(
popoverVisible.value = false
}
)
const hideShare = () => {
if (validatePwdFormat()) {
const hideShare = async () => {
const pwdValid = validatePwdFormat()
const uuidValid = await validateUuid()
if (pwdValid && uuidValid) {
popoverVisible.value = false
return
}
@ -170,6 +193,10 @@ const shareTips = computed(
const copyInfo = async () => {
if (shareEnable.value) {
try {
if (existErrorMsg('link-uuid-error-msg')) {
ElMessage.warning('链接格式错误,请重新填写!')
return
}
await toClipboard(linkAddr.value)
ElMessage.success(t('common.copy_success'))
} catch (e) {
@ -233,6 +260,9 @@ const enableSwitcher = () => {
}
const formatLinkAddr = () => {
linkAddr.value = formatLinkBase() + state.detailInfo.uuid
}
const formatLinkBase = () => {
let prefix = '/'
if (window.DataEaseBi?.baseUrl) {
prefix = window.DataEaseBi.baseUrl + '#'
@ -240,7 +270,7 @@ const formatLinkAddr = () => {
const href = window.location.href
prefix = href.substring(0, href.indexOf('#') + 1)
}
linkAddr.value = prefix + SHARE_BASE + state.detailInfo.uuid
return prefix + SHARE_BASE
}
const expEnableSwitcher = val => {
@ -316,25 +346,27 @@ const getUuid = () => {
const validatePwdFormat = () => {
if (!shareEnable.value || !passwdEnable.value || state.detailInfo.autoPwd) {
showPageError(null)
showPageError(null, pwdRef)
return true
}
const val = state.detailInfo.pwd
if (!val) {
showPageError('密码不能为空,请重新输入!')
showPageError('密码不能为空,请重新输入!', pwdRef)
return false
}
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{4,10}$/
if (!regex.test(val)) {
showPageError('密码必须是包含数字、字母、特殊字符[!@#$%^&*()_+]的4-10位字符串')
showPageError('密码必须是包含数字、字母、特殊字符[!@#$%^&*()_+]的4-10位字符串', pwdRef)
return false
}
showPageError(null)
showPageError(null, pwdRef)
resetPwdHandler(val, false)
return true
}
const showPageError = msg => {
const domRef = pwdRef
const showPageError = (msg, target, className?: string) => {
className = className || 'link-pwd-error-msg'
const fullClassName = `.${className}`
const domRef = target || pwdRef
if (!domRef.value) {
return
}
@ -342,7 +374,7 @@ const showPageError = msg => {
if (!msg) {
e.style = null
e.style.borderColor = null
const child = e.parentElement.querySelector('.link-pwd-error-msg')
const child = e.parentElement.querySelector(fullClassName)
if (child) {
e.parentElement['style'] = null
e.parentElement.removeChild(child)
@ -351,10 +383,10 @@ const showPageError = msg => {
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')
const child = e.parentElement.querySelector(fullClassName)
if (!child) {
const errorDom = document.createElement('div')
errorDom.className = 'link-pwd-error-msg'
errorDom.className = className
errorDom.innerText = msg
e.parentElement.appendChild(errorDom)
} else {
@ -362,12 +394,12 @@ const showPageError = msg => {
}
}
}
const existErrorMsg = () => {
return document.getElementsByClassName('link-pwd-error-msg')?.length
const existErrorMsg = (className: string) => {
return document.getElementsByClassName(className)?.length
}
const autoEnableSwitcher = val => {
if (val) {
showPageError(null)
showPageError(null, pwdRef)
resetPwd()
} else {
state.detailInfo.pwd = ''
@ -379,7 +411,7 @@ const autoEnableSwitcher = val => {
const copyPwd = async () => {
if (shareEnable.value && passwdEnable.value) {
if (!state.detailInfo.autoPwd && existErrorMsg()) {
if (!state.detailInfo.autoPwd && existErrorMsg('link-pwd-error-msg')) {
ElMessage.warning('密码格式错误,请重新填写!')
return
}
@ -393,6 +425,43 @@ const copyPwd = async () => {
ElMessage.warning(t('common.copy_unsupported'))
}
}
const editUuid = () => {
linkCustom.value = true
nextTick(() => {
if (linkUuidRef?.value) {
linkUuidRef.value.input.focus()
}
})
}
const validateUuid = async () => {
const val = state.detailInfo.uuid
const className = 'link-uuid-error-msg'
if (!val) {
showPageError('不能为空!', linkUuidRef, className)
return false
}
const regex = /^[a-zA-Z0-9]{8,16}$/
const result = regex.test(val)
if (!result) {
showPageError('仅支持8-16位(字母数字),请重新输入!', linkUuidRef, className)
} else {
const msg = await uuidValidateApi(val)
showPageError(msg, linkUuidRef, className)
return !msg
}
return result
}
const uuidValidateApi = async val => {
const url = '/share/editUuid'
const data = { resourceId: props.resourceId, uuid: val }
const res = await request.post({ url, data })
return res.data
}
const finishEditUuid = async () => {
const uuidValid = await validateUuid()
linkCustom.value = !uuidValid
}
const execute = () => {
share()
@ -436,6 +505,27 @@ defineExpose({
.text {
padding-bottom: 5px !important;
}
.custom-link-line {
display: flex;
margin-bottom: 16px;
align-items: center;
button {
width: 40px;
min-width: 40px;
margin-left: 8px;
height: 100%;
}
:deep(.link-uuid-error-msg) {
color: red;
position: absolute;
z-index: 9;
font-size: 10px;
height: 10px;
top: 25px;
width: 350px;
left: 0px;
}
}
}
.inline-share-item-picker {
display: flex;

View File

@ -1,10 +1,7 @@
package io.dataease.api.xpack.share;
import io.dataease.api.xpack.share.request.XpackShareExpRequest;
import io.dataease.api.xpack.share.request.XpackShareProxyRequest;
import io.dataease.api.xpack.share.request.XpackSharePwdRequest;
import io.dataease.api.xpack.share.request.*;
import io.dataease.api.visualization.request.VisualizationWorkbranchQueryRequest;
import io.dataease.api.xpack.share.request.XpackSharePwdValidator;
import io.dataease.api.xpack.share.vo.XpackShareGridVO;
import io.dataease.api.xpack.share.vo.XpackShareProxyVO;
import io.dataease.api.xpack.share.vo.XpackShareVO;
@ -61,4 +58,8 @@ public interface XpackShareApi {
@Operation(summary = "", hidden = true)
@GetMapping("/queryRelationByUserId/{uid}")
Map<String, String> queryRelationByUserId(@PathVariable("uid") Long uid);
@Operation(summary = "编辑分享uuid")
@PostMapping("/editUuid")
String editUuid(@RequestBody XpackShareUuidEditor editor);
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.xpack.share.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Schema(description = "分享UUID编辑器")
@Data
public class XpackShareUuidEditor implements Serializable {
@Schema(description = "资源ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long resourceId;
@Schema(description = "分享UUID", requiredMode = Schema.RequiredMode.REQUIRED)
private String uuid;
}