diff --git a/apps/chat/serializers/chat_authentication.py b/apps/chat/serializers/chat_authentication.py index b72f846a0a1..ff2c225e505 100644 --- a/apps/chat/serializers/chat_authentication.py +++ b/apps/chat/serializers/chat_authentication.py @@ -8,7 +8,6 @@ """ import uuid_utils.compat as uuid -from django.core import signing from django.core.cache import cache from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ @@ -116,6 +115,7 @@ def profile(): "max_attempts": 1, "enable_auth": False, "login_value": ["LOCAL"], + "default_value": "LOCAL", "rsa_key": get_key_pair_by_sql().get("key"), "meta": {}, "logo": "./favicon.ico", @@ -128,8 +128,9 @@ def profile(): return { **{ "max_attempts": auth_config.get("max_attempts", 1), - "enable_auth": portal.enable_auth, + "enable_auth": False, "login_value": auth_config.get("login_value", []), + "default_value": auth_config.get("type", "LOCAL"), "rsa_key": get_key_pair_by_sql().get("key"), "meta": {}, "logo": portal.logo or "./favicon.ico", diff --git a/apps/portal/api/portal.py b/apps/portal/api/portal.py index 70ffa107062..376c075e8c8 100644 --- a/apps/portal/api/portal.py +++ b/apps/portal/api/portal.py @@ -34,7 +34,6 @@ def get_request(): "tab_logo": {"type": "string", "format": "binary", "description": "浏览器Tab Logo"}, "enable_public_access": {"type": "boolean", "description": "是否开启公开访问"}, "enable_api": {"type": "boolean", "description": "是否开启API服务"}, - "enable_auth": {"type": "boolean", "description": "是否开启身份认证"}, "auth_config": {"type": "object", "description": "身份认证配置"}, "enable_cors": {"type": "boolean", "description": "是否开启跨域设置"}, "cross_domain_list": { diff --git a/apps/portal/serializers/portal.py b/apps/portal/serializers/portal.py index f45da17acd2..5e8aa04c2ce 100644 --- a/apps/portal/serializers/portal.py +++ b/apps/portal/serializers/portal.py @@ -50,7 +50,6 @@ class PortalSerializer(serializers.Serializer): enable_knowledge_base_api = serializers.BooleanField( required=False, label=_("enable knowledge base api"), help_text=_("enable knowledge base api") ) - enable_auth = serializers.BooleanField(required=False, label=_("enable auth"), help_text=_("enable auth")) auth_config = serializers.JSONField(required=False, label=_("auth config"), help_text=_("auth config")) enable_cors = serializers.BooleanField(required=False, label=_("enable cors"), help_text=_("enable cors")) cross_domain_list = serializers.ListSerializer( @@ -62,7 +61,7 @@ class PortalSerializer(serializers.Serializer): class Model(serializers.ModelSerializer): class Meta: model = Portal - fields = "__all__" + exclude = ("enable_auth",) @staticmethod def _is_license_valid(): @@ -111,11 +110,12 @@ def one(self): if portal is None: raise AppApiException(500, _("Portal configuration does not exist")) data = PortalSerializer.Model(portal).data - # 授权无效时不展示 auth_config,避免暴露认证配置 + # 授权无效时不展示 auth_config 与 logo,避免暴露认证配置与品牌信息 if self._is_license_valid(): data["auth_config"] = self._enrich_auth_config(data["auth_config"]) else: data["auth_config"] = {} + data["logo"] = "" return data def _upload_file(self, file_obj): @@ -152,8 +152,9 @@ def edit(self, instance, with_valid=True): if field == "auth_config" and isinstance(value, dict): value = {**(portal.auth_config or {}), **value} setattr(portal, field, value) + # Logo 为授权功能,授权无效时忽略 Logo 上传/修改 for field_name in ["logo"]: - if field_name in instance: + if field_name in instance and self._is_license_valid(): self._handle_file_field(portal, field_name, instance.get(field_name)) portal.save() return PortalSerializer.Model(portal).data @@ -166,13 +167,13 @@ def get_login_profile(): if portal is None: raise AppApiException(500, _("Portal configuration does not exist")) auth_config = portal.auth_config or {} + license_valid = PortalSerializer._is_license_valid() return { "name": portal.name, "description": portal.description or "", - "logo": portal.logo or "", - "enable_auth": portal.enable_auth, - "authentication_type": auth_config.get("type", "LOCAL") if portal.enable_auth else "", - "login_value": auth_config.get("login_value", []) if portal.enable_auth else [], - "max_attempts": auth_config.get("max_attempts", 1) if portal.enable_auth else 1, + "logo": (portal.logo or "") if license_valid else "", + "authentication_type": auth_config.get("type", "LOCAL"), + "login_value": auth_config.get("login_value", []), + "max_attempts": auth_config.get("max_attempts", 1), "rsa_key": get_key_pair_by_sql().get("key", ""), } diff --git a/apps/portal/urls.py b/apps/portal/urls.py index f9c47084691..6d9a95188c7 100644 --- a/apps/portal/urls.py +++ b/apps/portal/urls.py @@ -6,5 +6,4 @@ urlpatterns = [ path("portal", views.PortalView.as_view()), - path("portal/info", views.PortalInfoView.as_view()), ] diff --git a/apps/portal/views/portal.py b/apps/portal/views/portal.py index f5b4f441110..134bb28fd47 100644 --- a/apps/portal/views/portal.py +++ b/apps/portal/views/portal.py @@ -22,7 +22,6 @@ from portal.api.portal import PortalAPI from portal.serializers.portal import ( PortalSerializer, - PortalLoginSerializer, ) @@ -55,15 +54,3 @@ def get(self, request: Request): @has_permissions(PermissionConstants.PORTAL_EDIT, RoleConstants.ADMIN) def put(self, request: Request): return result.success(PortalSerializer(data=request.data).edit(request.data)) - - -class PortalInfoView(APIView): - @extend_schema( - methods=["GET"], - description=_("Get portal login info"), - summary=_("Get portal login info"), - operation_id=_("Get portal login info"), # type: ignore - tags=[_("Portal")], # type: ignore - ) - def get(self, request: Request): - return result.success(PortalLoginSerializer.get_login_profile()) diff --git a/apps/users/serializers/login.py b/apps/users/serializers/login.py index 8470a86bcce..e973b4df784 100644 --- a/apps/users/serializers/login.py +++ b/apps/users/serializers/login.py @@ -308,8 +308,6 @@ def chat_generate(username: str, type: str = "chat", access_token: str = ""): @staticmethod def portal_generate(username: str, type: str = "portal"): portal = Portal.objects.first() - if portal is None or not portal.enable_auth: - return {"captcha": ""} auth_config = portal.auth_config or {} max_attempts = auth_config.get("max_attempts", 1) key = f"{type}_{username}" diff --git a/ui/src/api/chat/auth.ts b/ui/src/api/chat/auth.ts index 76a129621ae..93afaae5536 100644 --- a/ui/src/api/chat/auth.ts +++ b/ui/src/api/chat/auth.ts @@ -76,7 +76,7 @@ const getDingTalkOauthCallback = (code: string, accessToken?: string) => { * 获取应用登录验证码;无需验证码时返回空字符串。 * 后端以 `accessToken` 查询参数接收应用令牌,且仅支持应用场景。 */ -const getCaptcha = (username: string, accessToken: string) => { +const getCaptcha = (username: string, accessToken?: string) => { return get('/v3/captcha', { username, accessToken }) } diff --git a/ui/src/api/types/chat-auth.ts b/ui/src/api/types/chat-auth.ts index 1cef88a07ab..7e2a9aaa8f9 100644 --- a/ui/src/api/types/chat-auth.ts +++ b/ui/src/api/types/chat-auth.ts @@ -8,6 +8,7 @@ export interface AuthProfile> { max_attempts: number enable_auth: boolean login_value: LoginMethod[] + default_value?: LoginMethod rsa_key: string logo: string custom_theme?: { theme_color?: string; header_font_color?: string } | null diff --git a/ui/src/views/chat/login/index.vue b/ui/src/views/chat/login/index.vue index da9f6d3aabc..e0f0468af1f 100644 --- a/ui/src/views/chat/login/index.vue +++ b/ui/src/views/chat/login/index.vue @@ -2,7 +2,7 @@ import { computed, onBeforeMount, ref } from 'vue' import { ArrowLeft } from '@element-plus/icons-vue' import { getDingTalkClientAuthCode } from '@/views/login/dingtalk-client-login' -import { useRoute, useRouter, type RouteLocationRaw } from 'vue-router' +import { type RouteLocationRaw, useRoute, useRouter } from 'vue-router' import { v7 as uuidv7 } from 'uuid' import ChatAuthApi from '@/api/chat/auth' import { CHAT_API_BASE_PATH } from '@/api/constants' @@ -25,11 +25,15 @@ const loginTitle = computed(() => { return (auth.authProfile?.meta as ApplicationAuthProfileMeta | undefined)?.application_name || '登录' }) -// 认证前置配置只有可用登录方式,默认登录方式依次取本地、LDAP、第一个可用方式 +// 认证前置配置只有可用登录方式,默认登录方式优先取后端配置,再依次回退本地、LDAP、第一个可用方式 const loginConfig = computed(() => { const loginMethods = auth.authProfile?.login_value?.length ? auth.authProfile.login_value : [LOGIN_METHOD.LOCAL] + const configuredDefault = auth.authProfile?.default_value const defaultLoginMethod = - [LOGIN_METHOD.LOCAL, LOGIN_METHOD.LDAP].find((method) => loginMethods.includes(method)) ?? loginMethods[0] ?? LOGIN_METHOD.LOCAL + (configuredDefault && loginMethods.includes(configuredDefault) ? configuredDefault : undefined) ?? + [LOGIN_METHOD.LOCAL, LOGIN_METHOD.LDAP].find((method) => loginMethods.includes(method)) ?? + loginMethods[0] ?? + LOGIN_METHOD.LOCAL return { default_value: defaultLoginMethod, login_methods: loginMethods, max_attempts: auth.authProfile?.max_attempts ?? 1 } }) @@ -75,7 +79,7 @@ const getExternalLoginUrl = (authType: LoginMethod): Promise => { if (!config.authEndpoint || !config.clientId) return '' const redirectUrl = `${config.redirectUrl}${queryString ? `?${queryString}` : ''}` const state = accessToken ? `${uuidv7()}_${accessToken}` : uuidv7() - let url = `${config.authEndpoint}?client_id=${config.clientId}&response_type=code&redirect_uri=${redirectUrl}&state=${state}` + let url = `${config.authEndpoint}?client_id=${config.clientId}&response_type=code&redirect_uri=${config.redirectUrl}&state=${state}` if (config.scope) url += `&scope=${config.scope}` return url }) @@ -102,11 +106,10 @@ const loginService: LoginService = { }, login: (loginRequest) => auth.login(loginRequest), ldapLogin: (loginForm) => auth.ldapLogin(loginForm), - // 验证码:后端按失败次数决定是否需要,无需时返回空字符串;目前仅应用场景支持 + // 验证码:后端按失败次数决定是否需要,无需时返回空字符串 getCaptcha: (username) => { - const { accessToken } = auth - if (!accessToken || !username.trim()) return Promise.resolve('') - return ChatAuthApi.getCaptcha(username, accessToken).then(({ captcha }) => captcha) + if (!username.trim()) return Promise.resolve('') + return ChatAuthApi.getCaptcha(username, auth.accessToken).then(({ captcha }) => captcha) }, getExternalLoginUrl, getQrCodeSources: () => ChatAuthApi.getQrCodeSources(), diff --git a/ui/src/views/system/chat-management/portal-setting/components/ButtonEditPortal.vue b/ui/src/views/system/chat-management/portal-setting/components/ButtonEditPortal.vue index 5966d589a57..86f17ffaa1f 100644 --- a/ui/src/views/system/chat-management/portal-setting/components/ButtonEditPortal.vue +++ b/ui/src/views/system/chat-management/portal-setting/components/ButtonEditPortal.vue @@ -4,7 +4,13 @@ import type { FormInstance, FormRules } from 'element-plus' import type { PortalSetting } from '@/api/types' import MkEditAvatar from '@/components/mk-edit-avatar/index.vue' -const props = defineProps<{ disabled: boolean; setting: PortalSetting; saving: boolean; save: (payload: FormData) => Promise }>() +const props = defineProps<{ + disabled: boolean + logoDisabled?: boolean + setting: PortalSetting + saving: boolean + save: (payload: FormData) => Promise +}>() /* 门户名称与 Logo */ const editVisible = ref(false) @@ -35,7 +41,7 @@ function handleSavePortalInfo() { if (!valid || props.disabled || props.saving) return const payload = new FormData() payload.append('name', portalForm.name.trim()) - if (logoChanged.value) payload.append('logo', logoFile.value || '') + if (logoChanged.value && !props.logoDisabled) payload.append('logo', logoFile.value || '') return props.save(payload).then(() => { editVisible.value = false }) @@ -68,7 +74,7 @@ function handleClosePortalEdit() { >
- + diff --git a/ui/src/views/system/chat-management/portal-setting/index.vue b/ui/src/views/system/chat-management/portal-setting/index.vue index 71c78b90423..af677db560f 100644 --- a/ui/src/views/system/chat-management/portal-setting/index.vue +++ b/ui/src/views/system/chat-management/portal-setting/index.vue @@ -105,6 +105,8 @@ onMounted(() => loadPortalSetting()) // 权限 控制 const canEditPortal = computed(() => perm.system.portal.edit()) +// Logo 为授权功能,授权无效(含社区版)时仅可查看,禁止上传或更换 Logo +const canEditLogo = computed(() => perm.system.portal.edit() && (auth.isPE || auth.isEE))