feat: 访问限制中新增身份验证设置

This commit is contained in:
wxg0103 2024-10-12 17:13:40 +08:00 committed by wxg0103
parent aa8e68a688
commit 7a0f15b4e4
8 changed files with 403 additions and 207 deletions

View File

@ -312,6 +312,20 @@ class ApplicationSerializer(serializers.Serializer):
if 'show_source' in instance and instance.get('show_source') is not None:
application_access_token.show_source = instance.get('show_source')
application_access_token.save()
application_setting_model = DBModelManage.get_model('application_setting')
X_PACK_LICENSE_IS_VALID = (settings.XPACK_LICENSE_IS_VALID if hasattr(settings,
'XPACK_LICENSE_IS_VALID') else False)
if application_setting_model is not None and X_PACK_LICENSE_IS_VALID:
application_setting, _ = application_setting_model.objects.get_or_create(
application_id=self.data.get('application_id'))
if application_setting is not None:
application_setting.authentication = instance.get('authentication')
application_setting.authentication_value = {
"type": "password",
"value": instance.get('authentication_value')
}
application_setting.save()
get_application_access_token(application_access_token.access_token, False)
return self.one(with_valid=False)
@ -734,7 +748,8 @@ class ApplicationSerializer(serializers.Serializer):
'draggable': application_setting.draggable,
'show_guide': application_setting.show_guide,
'avatar': application_setting.avatar,
'float_icon': application_setting.float_icon}
'float_icon': application_setting.float_icon,
'authentication': application_setting.authentication}
return ApplicationSerializer.Query.reset_application(
{**ApplicationSerializer.ApplicationModel(application).data,
'stt_model_id': application.stt_model_id,

View File

@ -387,6 +387,15 @@ const updatePlatformStatus: (application_id: string, data: any) => Promise<Resul
) => {
return post(`/platform/${application_id}/status`, data)
}
/**
*
*/
const validatePassword: (application_id: string, password: string) => Promise<Result<any>> = (
application_id,
password
) => {
return get(`/application/${application_id}/auth/${password}`, undefined)
}
export default {
getAllAppilcation,
@ -419,5 +428,6 @@ export default {
getPlatformStatus,
getPlatformConfig,
updatePlatformConfig,
updatePlatformStatus
updatePlatformStatus,
validatePassword
}

View File

@ -65,6 +65,8 @@ export default {
dialogTitle: 'Access Restrictions',
showSourceLabel: 'Show Source',
clientQueryLimitLabel: 'Each Client Query Limit',
authentication: 'Authentication',
authenticationValue: 'Authentication Password',
timesDays: 'Times/Day',
whitelistLabel: 'Whitelist',
whitelistPlaceholder:

View File

@ -65,6 +65,8 @@ export default {
showSourceLabel: '显示知识来源',
clientQueryLimitLabel: '每个客户端提问限制',
timesDays: '次/天',
authentication: '身份验证',
authenticationValue: '验证密码',
whitelistLabel: '白名单',
whitelistPlaceholder:
'请输入允许嵌入第三方的源地址,一行一个,如:\nhttp://127.0.0.1:5678\nhttps://dataease.io',

View File

@ -119,6 +119,18 @@ const useApplicationStore = defineStore({
reject(error)
})
})
},
async validatePassword(id: string, password: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
applicationApi
.validatePassword(id, password)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
}
}
})

View File

@ -27,6 +27,30 @@
$t('views.applicationOverview.appInfo.LimitDialog.timesDays')
}}</span>
</el-form-item>
<!-- 身份验证 -->
<el-form-item
:label="$t('views.applicationOverview.appInfo.LimitDialog.authentication')"
v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')"
>
<el-switch size="small" v-model="form.authentication"></el-switch>
</el-form-item>
<el-form-item
v-if="form.authentication"
:label="$t('views.applicationOverview.appInfo.LimitDialog.authenticationValue')"
v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')"
>
<el-input
v-model="form.authentication_value"
readonly
style="width: 300px; margin-right: 10px"
></el-input>
<el-button type="primary" text @click="copyClick(form.authentication_value)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
<el-button @click="refreshAuthentication" type="primary" text style="margin-left: 1px">
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-form-item>
<el-form-item
:label="$t('views.applicationOverview.appInfo.LimitDialog.whitelistLabel')"
@click.prevent
@ -61,6 +85,8 @@ import type { FormInstance, FormRules } from 'element-plus'
import applicationApi from '@/api/application'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
import { copyClick } from '@/utils/clipboard'
import { ComplexPermission } from '@/utils/permission/type'
const route = useRoute()
const {
@ -73,7 +99,9 @@ const limitFormRef = ref()
const form = ref<any>({
access_num: 0,
white_active: true,
white_list: ''
white_list: '',
authentication_value: '',
authentication: false
})
const dialogVisible = ref<boolean>(false)
@ -93,6 +121,8 @@ const open = (data: any) => {
form.value.access_num = data.access_num
form.value.white_active = data.white_active
form.value.white_list = data.white_list?.length ? data.white_list?.join('\n') : ''
form.value.authentication_value = data.authentication_value
form.value.authentication = data.authentication
dialogVisible.value = true
}
@ -103,7 +133,9 @@ const submit = async (formEl: FormInstance | undefined) => {
const obj = {
white_list: form.value.white_list ? form.value.white_list.split('\n') : [],
white_active: form.value.white_active,
access_num: form.value.access_num
access_num: form.value.access_num,
authentication: form.value.authentication,
authentication_value: form.value.authentication_value
}
applicationApi.putAccessToken(id as string, obj, loading).then((res) => {
emit('refresh')
@ -114,6 +146,17 @@ const submit = async (formEl: FormInstance | undefined) => {
}
})
}
function generateAuthenticationValue(length: number = 10) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const randomValues = new Uint8Array(length)
window.crypto.getRandomValues(randomValues)
return Array.from(randomValues)
.map((value) => chars[value % chars.length])
.join('')
}
function refreshAuthentication() {
form.value.authentication_value = generateAuthenticationValue()
}
defineExpose({ open })
</script>

View File

@ -1,101 +1,130 @@
<template>
<div class="chat-embed layout-bg" v-loading="loading">
<div class="chat-embed__header" :class="!isDefaultTheme ? 'custom-header' : ''">
<div class="chat-width flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
<div class="chat-embed__main">
<AiChat
ref="AiChatRef"
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
class="AiChat-embed"
>
<template #operateBefore>
<div class="chat-width">
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4"></span>
</el-button>
</div>
</template>
</AiChat>
</div>
<!-- 历史记录弹出层 -->
<div
v-if="applicationDetail.show_history || !user.isEnterprise()"
@click.prevent.stop="show = !show"
class="chat-popover-button cursor color-secondary"
<el-dialog
v-model="isPasswordDialogVisible"
width="480px"
height="236px"
title="输入密码打开链接"
custom-class="no-close-button"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
center
:modal="true"
>
<AppIcon iconName="app-history-outlined"></AppIcon>
</div>
<el-input
style="width: 400px; height: 40px"
v-model="password"
:placeholder="$t('login.ldap.passwordPlaceholder')"
show-password
/>
<span class="input-error" v-if="passwordError">{{ passwordError }}</span>
<el-button
type="primary"
@click="validatePassword"
style="width: 400px; height: 40px; margin-top: 24px"
>确定</el-button
>
</el-dialog>
<el-collapse-transition>
<div v-show="show" class="chat-popover w-full" v-click-outside="clickoutside">
<div class="border-b p-16-24">
<span>历史记录</span>
</div>
<el-scrollbar max-height="300">
<div class="p-8">
<common-list
:data="chatLogeData"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
<div v-if="isAuthenticated">
<div class="chat-embed__header" :class="!isDefaultTheme ? 'custom-header' : ''">
<div class="chat-width flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-if="mouseId === row.id && row.id !== 'new'">
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">暂无历史记录</el-text>
</div>
</template>
</common-list>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<div v-if="chatLogeData.length" class="gradient-divider lighter mt-8">
<span>仅显示最近 20 条对话</span>
</div>
</el-scrollbar>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
</el-collapse-transition>
<div class="chat-popover-mask" v-show="show"></div>
<div class="chat-embed__main">
<AiChat
ref="AiChatRef"
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
class="AiChat-embed"
>
<template #operateBefore>
<div class="chat-width">
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4"></span>
</el-button>
</div>
</template>
</AiChat>
</div>
<!-- 历史记录弹出层 -->
<div
v-if="applicationDetail.show_history || !user.isEnterprise()"
@click.prevent.stop="show = !show"
class="chat-popover-button cursor color-secondary"
>
<AppIcon iconName="app-history-outlined"></AppIcon>
</div>
<el-collapse-transition>
<div v-show="show" class="chat-popover w-full" v-click-outside="clickoutside">
<div class="border-b p-16-24">
<span>历史记录</span>
</div>
<el-scrollbar max-height="300">
<div class="p-8">
<common-list
:data="chatLogeData"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-if="mouseId === row.id && row.id !== 'new'">
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">暂无历史记录</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogeData.length" class="gradient-divider lighter mt-8">
<span>仅显示最近 20 条对话</span>
</div>
</el-scrollbar>
</div>
</el-collapse-transition>
<div class="chat-popover-mask" v-show="show"></div>
</div>
</div>
</template>
<script setup lang="ts">
@ -121,6 +150,10 @@ const applicationDetail = ref<any>({})
const applicationAvailable = ref<boolean>(true)
const chatLogeData = ref<any[]>([])
const show = ref(false)
const isPasswordDialogVisible = ref(false)
const password = ref('')
const passwordError = ref('')
const isAuthenticated = ref(false)
const paginationConfig = reactive({
current_page: 1,
@ -171,6 +204,20 @@ function newChat() {
currentRecordList.value = []
currentChatId.value = 'new'
}
function validatePassword() {
if (!password.value) {
passwordError.value = '密码不能为空'
return //
}
application.validatePassword(applicationDetail?.value.id, password.value).then((res: any) => {
if (res?.data.is_valid) {
isAuthenticated.value = true
isPasswordDialogVisible.value = false
} else {
passwordError.value = '密码错误'
}
})
}
function getAccessToken(token: string) {
application
@ -189,6 +236,12 @@ function getAppProfile() {
.asyncGetAppProfile(loading)
.then((res: any) => {
applicationDetail.value = res.data
if (user.isEnterprise()) {
isPasswordDialogVisible.value = applicationDetail?.value.authentication
}
if (!isPasswordDialogVisible.value) {
isAuthenticated.value = true
}
if (res.data?.show_history || !user.isEnterprise()) {
getChatLog(applicationDetail.value.id)
}

View File

@ -1,128 +1,155 @@
<template>
<div class="chat-pc layout-bg" :class="classObj" v-loading="loading">
<div class="chat-pc__header" :class="!isDefaultTheme ? 'custom-header' : ''">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<el-dialog
v-model="isPasswordDialogVisible"
width="480px"
height="236px"
title="输入密码打开链接"
custom-class="no-close-button"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
center
:modal="true"
>
<el-input
style="width: 400px; height: 40px"
v-model="password"
:placeholder="$t('login.ldap.passwordPlaceholder')"
show-password
/>
<span class="input-error" v-if="passwordError">{{ passwordError }}</span>
<el-button
type="primary"
@click="validatePassword"
style="width: 400px; height: 40px; margin-top: 24px"
>确定</el-button
>
</el-dialog>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
<div class="flex">
<div class="chat-pc__left border-r">
<div class="p-24 pb-0">
<el-button class="add-button w-full primary" @click="newChat">
<el-icon>
<Plus />
</el-icon>
<span class="ml-4">新建对话</span>
</el-button>
<p class="mt-20 mb-8">历史记录</p>
<div v-if="isAuthenticated">
<div class="chat-pc__header" :class="!isDefaultTheme ? 'custom-header' : ''">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
<div class="left-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:data="chatLogeData"
class="mt-8"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-if="mouseId === row.id && row.id !== 'new'">
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<div class="flex">
<div class="chat-pc__left border-r">
<div class="p-24 pb-0">
<el-button class="add-button w-full primary" @click="newChat">
<el-icon>
<Plus />
</el-icon>
<span class="ml-4">新建对话</span>
</el-button>
<p class="mt-20 mb-8">历史记录</p>
</div>
<div class="left-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:data="chatLogeData"
class="mt-8"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-if="mouseId === row.id && row.id !== 'new'">
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</template>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">暂无历史记录</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogeData.length" class="gradient-divider lighter mt-8">
<span>仅显示最近 20 条对话</span>
</div>
</el-scrollbar>
<template #empty>
<div class="text-center">
<el-text type="info">暂无历史记录</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogeData.length" class="gradient-divider lighter mt-8">
<span>仅显示最近 20 条对话</span>
</div>
</el-scrollbar>
</div>
</div>
</div>
<div class="chat-pc__right">
<div class="right-header border-b mb-24 p-16-24 flex-between">
<h4 class="ellipsis-1" style="width: 70%">
{{ currentChatName }}
</h4>
<div class="chat-pc__right">
<div class="right-header border-b mb-24 p-16-24 flex-between">
<h4 class="ellipsis-1" style="width: 70%">
{{ currentChatName }}
</h4>
<span class="flex align-center" v-if="currentRecordList.length">
<AppIcon
v-if="paginationConfig.total"
iconName="app-chat-record"
class="info mr-8"
style="font-size: 16px"
></AppIcon>
<span v-if="paginationConfig.total" class="lighter">
{{ paginationConfig.total }} 条提问
<span class="flex align-center" v-if="currentRecordList.length">
<AppIcon
v-if="paginationConfig.total"
iconName="app-chat-record"
class="info mr-8"
style="font-size: 16px"
></AppIcon>
<span v-if="paginationConfig.total" class="lighter">
{{ paginationConfig.total }} 条提问
</span>
<el-dropdown class="ml-8">
<AppIcon iconName="app-export" class="cursor" title="导出聊天记录"></AppIcon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="exportMarkdown">导出 Markdown</el-dropdown-item>
<el-dropdown-item @click="exportHTML">导出 HTML</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
<el-dropdown class="ml-8">
<AppIcon iconName="app-export" class="cursor" title="导出聊天记录"></AppIcon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="exportMarkdown">导出 Markdown</el-dropdown-item>
<el-dropdown-item @click="exportHTML">导出 HTML</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div class="right-height">
<!-- 对话 -->
<AiChat
ref="AiChatRef"
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
>
</AiChat>
</div>
<div class="right-height">
<AiChat
ref="AiChatRef"
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
>
</AiChat>
</div>
</div>
</div>
</div>
<div class="collapse">
<el-button @click="isCollapse = !isCollapse">
<el-icon> <component :is="isCollapse ? 'Fold' : 'Expand'" /></el-icon>
</el-button>
<div class="collapse">
<el-button @click="isCollapse = !isCollapse">
<el-icon> <component :is="isCollapse ? 'Fold' : 'Expand'" /></el-icon>
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick, computed } from 'vue'
import { useRoute } from 'vue-router'
@ -130,8 +157,11 @@ import { marked } from 'marked'
import { saveAs } from 'file-saver'
import { isAppIcon } from '@/utils/application'
import useStore from '@/stores'
import useResize from '@/layout/hooks/useResize'
import type { FormInstance, FormRules } from 'element-plus'
import { t } from '@/locales'
import authApi from '@/api/auth-setting'
import { MsgSuccess } from '@/utils/message'
useResize()
const route = useRoute()
@ -147,6 +177,10 @@ const isDefaultTheme = computed(() => {
})
const isCollapse = ref(false)
const isPasswordDialogVisible = ref(false)
const password = ref('')
const passwordError = ref('')
const isAuthenticated = ref(false)
const classObj = computed(() => {
return {
@ -225,6 +259,12 @@ function getAppProfile() {
.asyncGetAppProfile(loading)
.then((res: any) => {
applicationDetail.value = res.data
if (user.isEnterprise()) {
isPasswordDialogVisible.value = applicationDetail?.value.authentication
}
if (!isPasswordDialogVisible.value) {
isAuthenticated.value = true
}
if (res.data?.show_history || !user.isEnterprise()) {
getChatLog(applicationDetail.value.id)
}
@ -336,6 +376,21 @@ async function exportHTML(): Promise<void> {
saveAs(blob, suggestedName)
}
function validatePassword() {
if (!password.value) {
passwordError.value = '密码不能为空'
return //
}
application.validatePassword(applicationDetail?.value.id, password.value).then((res: any) => {
if (res?.data.is_valid) {
isAuthenticated.value = true
isPasswordDialogVisible.value = false
} else {
passwordError.value = '密码错误'
}
})
}
onMounted(() => {
user.changeUserType(2)
getAccessToken(accessToken)
@ -455,4 +510,8 @@ onMounted(() => {
}
}
}
.input-error {
color: red;
display: block;
}
</style>