diff --git a/apps/chat/serializers/chat_authentication.py b/apps/chat/serializers/chat_authentication.py index 7283afafd..60a8a145d 100644 --- a/apps/chat/serializers/chat_authentication.py +++ b/apps/chat/serializers/chat_authentication.py @@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from application.models import ApplicationAccessToken, ChatUserType, Application, ApplicationVersion +from application.models.application_api_key import ApplicationApiKey from application.serializers.application import ApplicationSerializerModel from common.auth.common import ChatUserToken, ChatAuthentication from common.auth.handle.impl.user_token import UserToken @@ -26,12 +27,27 @@ from common.utils.rsa_util import get_key_pair_by_sql class AnonymousAuthenticationSerializer(serializers.Serializer): access_token = serializers.CharField(required=True, label=_("access_token")) + api_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("api key")) @staticmethod def get_platform_user(request): + if request is None: + return None platform_auth = request.META.get('HTTP_X_PLATFORM_AUTHORIZATION') if platform_auth is None or not platform_auth.startswith('Bearer '): return None + + @staticmethod + def validate_application_api_key(application_id, api_key): + if not api_key: + raise AppUnauthorizedFailed(401, _("Authentication information is incorrect")) + application_api_key = QuerySet(ApplicationApiKey).filter( + application_id=application_id, + secret_key=api_key, + is_active=True + ).first() + if application_api_key is None: + raise AppUnauthorizedFailed(401, _("Authentication information is incorrect")) token = platform_auth[7:] token_details = None @@ -67,6 +83,8 @@ class AnonymousAuthenticationSerializer(serializers.Serializer): chat_user_type = ChatUserType.PLATFORM_USER.value user_id = platform_user.id else: + api_key = self.data.get('api_key') or request.META.get('HTTP_X_APPLICATION_API_KEY') + self.validate_application_api_key(application_access_token.application_id, api_key) chat_user_id = token_details.get('chat_user_id') or str(uuid.uuid7()) chat_user_type = ChatUserType.ANONYMOUS_USER.value user_id = None @@ -80,8 +98,9 @@ class AnonymousAuthenticationSerializer(serializers.Serializer): class AuthProfileSerializer(serializers.Serializer): access_token = serializers.CharField(required=True, label=_("access_token")) + api_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("api key")) - def profile(self): + def profile(self, request=None): self.is_valid(raise_exception=True) access_token = self.data.get("access_token") application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first() @@ -90,6 +109,8 @@ class AuthProfileSerializer(serializers.Serializer): if not application_access_token.is_active: raise NotFound404(404, _("Invalid access_token")) application_id = application_access_token.application_id + if AnonymousAuthenticationSerializer.get_platform_user(request) is None: + AnonymousAuthenticationSerializer.validate_application_api_key(application_id, self.data.get("api_key")) profile = { 'authentication': False } diff --git a/apps/chat/serializers/chat_embed_serializers.py b/apps/chat/serializers/chat_embed_serializers.py index a3f25af7f..8a6121157 100644 --- a/apps/chat/serializers/chat_embed_serializers.py +++ b/apps/chat/serializers/chat_embed_serializers.py @@ -103,4 +103,6 @@ class ChatEmbedSerializer(serializers.Serializer): query += f"&{field['variable']}={params[field['variable']]}" if 'asker' in params: query += f"&asker={params.get('asker')}" + if 'api_key' in params: + query += f"&api_key={params.get('api_key')}" return query diff --git a/apps/chat/views/chat.py b/apps/chat/views/chat.py index 5e5d29f9d..cc6ac8f30 100644 --- a/apps/chat/views/chat.py +++ b/apps/chat/views/chat.py @@ -138,7 +138,10 @@ class AuthProfile(APIView): ) def get(self, request: Request): return result.success( - AuthProfileSerializer(data={'access_token': request.query_params.get("access_token")}).profile()) + AuthProfileSerializer(data={ + 'access_token': request.query_params.get("access_token"), + 'api_key': request.query_params.get("api_key") + }).profile(request)) class ChatView(APIView): diff --git a/ui/src/api/chat/chat.ts b/ui/src/api/chat/chat.ts index 092de45ac..58aa4aa74 100644 --- a/ui/src/api/chat/chat.ts +++ b/ui/src/api/chat/chat.ts @@ -47,11 +47,12 @@ const chat: (chat_id: string, data: any) => Promise = (chat_id, data) => { /** * 应用认证信息 */ -const chatProfile: (assessToken: string, loading?: Ref) => Promise> = ( +const chatProfile: (assessToken: string, apiKey?: string, loading?: Ref) => Promise> = ( assessToken, + apiKey, loading, ) => { - return get('/profile', {access_token: assessToken}, loading) + return get('/profile', {access_token: assessToken, api_key: apiKey}, loading) } /** * 匿名认证 @@ -61,9 +62,10 @@ const chatProfile: (assessToken: string, loading?: Ref) => Promise, -) => Promise> = (assessToken, loading) => { - return post('/auth/anonymous', {access_token: assessToken}, {}, loading) +) => Promise> = (assessToken, apiKey, loading) => { + return post('/auth/anonymous', {access_token: assessToken, api_key: apiKey}, {}, loading) } /** * 密码认证 diff --git a/ui/src/router/chat/index.ts b/ui/src/router/chat/index.ts index 5cd3d945c..b92729be5 100644 --- a/ui/src/router/chat/index.ts +++ b/ui/src/router/chat/index.ts @@ -27,6 +27,7 @@ router.beforeEach( const { chatUser } = useStore() if (['login', 'chat'].includes(to.name ? to.name.toString() : '')) { chatUser.setAccessToken(to.params.accessToken.toString()) + chatUser.setApiKey(typeof to.query.api_key === 'string' ? to.query.api_key : undefined) } else { next({ path: '/404', diff --git a/ui/src/stores/modules/chat-user.ts b/ui/src/stores/modules/chat-user.ts index a3dbb5ff1..e9057e8ab 100644 --- a/ui/src/stores/modules/chat-user.ts +++ b/ui/src/stores/modules/chat-user.ts @@ -17,6 +17,7 @@ interface Chat { token?: string accessToken?: string chatUserType?: string + apiKey?: string } const useChatUserStore = defineStore('chat-user', { @@ -32,8 +33,11 @@ const useChatUserStore = defineStore('chat-user', { setAccessToken(accessToken: string) { this.accessToken = accessToken }, + setApiKey(apiKey?: string) { + this.apiKey = apiKey + }, getChatProfile() { - return ChatAPI.chatProfile(this.accessToken as string).then((ok) => { + return ChatAPI.chatProfile(this.accessToken as string, this.apiKey).then((ok) => { this.chat_profile = ok.data return this.chat_profile @@ -92,7 +96,7 @@ const useChatUserStore = defineStore('chat-user', { *匿名认证 */ anonymousAuthentication() { - return ChatAPI.anonymousAuthentication(this.accessToken as string).then((ok) => { + return ChatAPI.anonymousAuthentication(this.accessToken as string, this.apiKey).then((ok) => { const data = typeof ok.data === 'string' ? { token: ok.data, chat_user_type: undefined } : ok.data this.setToken(data.token) this.setChatUserType(data.chat_user_type) diff --git a/ui/src/views/application-overview/component/APIKeyDialog.vue b/ui/src/views/application-overview/component/APIKeyDialog.vue index 676366261..e0eb1018a 100644 --- a/ui/src/views/application-overview/component/APIKeyDialog.vue +++ b/ui/src/views/application-overview/component/APIKeyDialog.vue @@ -79,7 +79,7 @@ const apiType = computed(() => { } }) -const emit = defineEmits(['addData']) +const emit = defineEmits(['addData', 'refresh']) const SettingAPIKeyDialogRef = ref() const dialogVisible = ref(false) @@ -112,6 +112,7 @@ function deleteApiKey(row: any) { .then(() => { MsgSuccess(t('common.deleteSuccess')) getApiKeyList() + emit('refresh') }) }) .catch(() => {}) @@ -129,6 +130,7 @@ async function changeState(row: any) { .then(() => { MsgSuccess(str) getApiKeyList() + emit('refresh') return true }) .catch(() => { @@ -141,6 +143,7 @@ function createApiKey() { .postAPIKey(id as string, loading) .then(() => { getApiKeyList() + emit('refresh') }) } diff --git a/ui/src/views/application-overview/component/EmbedDialog.vue b/ui/src/views/application-overview/component/EmbedDialog.vue index 52b75abc7..d7eca4e69 100644 --- a/ui/src/views/application-overview/component/EmbedDialog.vue +++ b/ui/src/views/application-overview/component/EmbedDialog.vue @@ -90,6 +90,7 @@ const { application } = useStore() const props = defineProps({ data: Object, apiInputParams: String, + apiKey: String, }) const emit = defineEmits(['addData']) @@ -101,10 +102,12 @@ const source1 = ref('') const source2 = ref('') const source3 = ref('') -const urlParams1 = computed(() => (props.apiInputParams ? '?' + props.apiInputParams : '')) -const urlParams2 = computed(() => (props.apiInputParams ? '&' + props.apiInputParams : '')) +const apiKeyParam = computed(() => props.apiKey ? `api_key=${encodeURIComponent(props.apiKey)}` : '') +const mergedParams = computed(() => [apiKeyParam.value, props.apiInputParams].filter(Boolean).join('&')) +const urlParams1 = computed(() => (mergedParams.value ? '?' + mergedParams.value : '')) +const urlParams2 = computed(() => (mergedParams.value ? '&' + mergedParams.value : '')) const urlParams3 = computed(() => - props.apiInputParams ? '?mode=mobile&' + props.apiInputParams : '?mode=mobile', + mergedParams.value ? '?mode=mobile&' + mergedParams.value : '?mode=mobile', ) watch(dialogVisible, (bool) => { if (!bool) { diff --git a/ui/src/views/application-overview/index.vue b/ui/src/views/application-overview/index.vue index 837c5f65e..4f56df2c2 100644 --- a/ui/src/views/application-overview/index.vue +++ b/ui/src/views/application-overview/index.vue @@ -184,8 +184,9 @@ ref="EmbedDialogRef" :data="detail" :api-input-params="mapToUrlParams(apiInputParams)" + :api-key="publicApiKey" /> - + @@ -238,13 +239,18 @@ const APIKeyDialogRef = ref() const EmbedDialogRef = ref() const accessToken = ref({}) +const publicApiKey = ref('') const detail = ref(null) const loading = ref(false) -const urlParams = computed(() => - mapToUrlParams(apiInputParams.value) ? '?' + mapToUrlParams(apiInputParams.value) : '', -) +const urlParams = computed(() => { + const params = [ + publicApiKey.value ? `api_key=${encodeURIComponent(publicApiKey.value)}` : '', + mapToUrlParams(apiInputParams.value), + ].filter(Boolean) + return params.length ? '?' + params.join('&') : '' +}) const shareUrl = computed( () => `${window.location.origin}${window.MaxKB.chatPrefix}/` + @@ -424,6 +430,24 @@ function openDialog() { EmbedDialogRef.value.open(accessToken.value?.access_token) } +function getPublicApiKey() { + publicApiKey.value = '' + loadSharedApi({type: 'applicationKey', systemType: apiType.value}) + .getAPIKey(id as string, loading) + .then((res: any) => { + const key = res.data?.find((item: any) => item.is_active) || res.data?.[0] + if (key) { + publicApiKey.value = key.secret_key + return + } + loadSharedApi({type: 'applicationKey', systemType: apiType.value}) + .postAPIKey(id as string, loading) + .then((apiKeyRes: any) => { + publicApiKey.value = apiKeyRes.data?.secret_key || '' + }) + }) +} + function getAccessToken() { loadSharedApi({type: 'application', systemType: apiType.value}) .getAccessToken(id, loading) @@ -468,6 +492,7 @@ function refresh() { onMounted(() => { getDetail() getAccessToken() + getPublicApiKey() changeDayHandle(history_day.value) })