From d186daa13172ea0c745288978a8fee26ec7ec2bf Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 20 May 2025 12:07:50 +0800 Subject: [PATCH] E-300 (#19726) Signed-off-by: -LAN- Co-authored-by: Hash Brown Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: GareArc Co-authored-by: Byron.wang Co-authored-by: Joel Co-authored-by: -LAN- Co-authored-by: Garfield Dai Co-authored-by: KVOJJJin Co-authored-by: Alexi.F <654973939@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: kautsar_masuara <61046989+izon-masuara@users.noreply.github.com> Co-authored-by: achmad-kautsar Co-authored-by: Xin Zhang Co-authored-by: kelvintsim <83445753+kelvintsim@users.noreply.github.com> Co-authored-by: zxhlyh Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com> --- .github/workflows/style.yml | 1 + api/controllers/console/app/app.py | 24 +- .../console/auth/forgot_password.py | 7 +- api/controllers/console/auth/login.py | 21 +- api/controllers/console/error.py | 12 + api/controllers/console/explore/error.py | 6 + .../console/explore/installed_app.py | 21 ++ api/controllers/console/explore/wraps.py | 36 ++- api/controllers/console/workspace/members.py | 8 + api/controllers/inner_api/__init__.py | 1 + api/controllers/inner_api/mail.py | 27 +++ api/controllers/web/app.py | 51 ++++- api/controllers/web/error.py | 10 +- api/controllers/web/login.py | 120 ++++++++++ api/controllers/web/passport.py | 10 +- api/controllers/web/wraps.py | 64 ++++-- api/fields/app_fields.py | 3 + api/services/account_service.py | 12 +- api/services/app_service.py | 24 +- api/services/enterprise/enterprise_service.py | 83 ++++++- api/services/enterprise/mail_service.py | 18 ++ api/services/errors/workspace.py | 4 + api/services/feature_service.py | 132 ++++++++--- api/services/webapp_auth_service.py | 141 ++++++++++++ api/tasks/mail_email_code_login.py | 19 +- api/tasks/mail_enterprise_task.py | 33 +++ api/tasks/mail_invite_member_task.py | 55 +++-- api/tasks/mail_reset_password_task.py | 25 ++- .../email_code_login_mail_template_en-US.html | 70 ++++++ .../email_code_login_mail_template_zh-CN.html | 70 ++++++ .../invite_member_mail_template_en-US.html | 69 ++++++ .../invite_member_mail_template_zh-CN.html | 69 ++++++ .../reset_password_mail_template_en-US.html | 70 ++++++ .../reset_password_mail_template_zh-CN.html | 70 ++++++ .../(appDetailLayout)/[appId]/layout-main.tsx | 17 +- .../[appId]/overview/cardView.tsx | 26 +-- .../app/(appDetailLayout)/layout.tsx | 7 +- web/app/(commonLayout)/apps/AppCard.tsx | 84 +++++-- web/app/(commonLayout)/apps/Apps.tsx | 1 - web/app/(commonLayout)/apps/layout.tsx | 12 + web/app/(commonLayout)/apps/page.tsx | 10 +- .../[datasetId]/layout-main.tsx | 6 +- web/app/(commonLayout)/datasets/Container.tsx | 8 +- web/app/(commonLayout)/datasets/Datasets.tsx | 8 +- web/app/(commonLayout)/datasets/page.tsx | 7 +- .../datasets/template/template.en.mdx | 2 +- .../datasets/template/template.zh.mdx | 2 +- web/app/(commonLayout)/explore/layout.tsx | 14 +- web/app/(commonLayout)/layout.tsx | 5 - web/app/(commonLayout)/tools/page.tsx | 18 +- web/app/(shareLayout)/webapp-signin/page.tsx | 80 +++++-- web/app/account/account-page/index.tsx | 5 +- web/app/account/layout.tsx | 5 - web/app/account/page.tsx | 5 + web/app/activate/activateForm.tsx | 2 + web/app/activate/page.tsx | 7 +- web/app/components/app-sidebar/app-info.tsx | 5 +- web/app/components/app-sidebar/index.tsx | 2 +- .../access-control-dialog.tsx | 61 ++++++ .../access-control-item.tsx | 30 +++ .../add-member-or-group-pop.tsx | 204 +++++++++++++++++ .../app/app-access-control/index.tsx | 102 +++++++++ .../specific-groups-or-members.tsx | 139 ++++++++++++ .../components/app/app-publisher/index.tsx | 205 ++++++++++++------ .../app/app-publisher/suggested-action.tsx | 42 ++-- web/app/components/app/overview/appCard.tsx | 62 +++++- .../app/overview/settings/index.tsx | 40 +--- web/app/components/base/app-unavailable.tsx | 2 +- .../base/chat/chat-with-history/context.tsx | 5 + .../base/chat/chat-with-history/hooks.tsx | 18 +- .../base/chat/chat-with-history/index.tsx | 16 +- .../chat/chat-with-history/sidebar/index.tsx | 55 ++--- .../base/chat/embedded-chatbot/context.tsx | 5 + .../base/chat/embedded-chatbot/hooks.tsx | 18 +- .../base/chat/embedded-chatbot/index.tsx | 17 +- web/app/components/base/logo/dify-logo.tsx | 9 +- web/app/components/base/svg-gallery/index.tsx | 2 +- web/app/components/base/tooltip/index.tsx | 1 + web/app/components/billing/type.ts | 5 + .../datasets/create/step-one/index.tsx | 36 +-- .../develop/template/template.zh.mdx | 2 +- .../develop/template/template_workflow.zh.mdx | 2 +- web/app/components/explore/index.tsx | 8 +- .../explore/installed-app/index.tsx | 6 +- .../header/account-dropdown/index.tsx | 129 +++++------ .../account-setting/members-page/index.tsx | 4 +- .../members-page/invite-modal/index.tsx | 32 ++- .../model-provider-page/index.tsx | 4 +- .../components/header/license-env/index.tsx | 5 +- .../plugins/plugin-page/context.tsx | 4 +- .../plugins/plugin-page/empty/index.tsx | 4 +- .../components/plugins/plugin-page/index.tsx | 8 +- .../plugin-page/install-plugin-dropdown.tsx | 4 +- .../plugins/plugin-page/use-permission.ts | 4 +- .../share/text-generation/index.tsx | 43 ++-- .../share/text-generation/info-modal.tsx | 2 +- .../share/text-generation/menu-dropdown.tsx | 16 +- web/app/components/tools/provider-list.tsx | 8 +- .../workflow-header/features-trigger.tsx | 14 +- .../workflow/block-selector/all-tools.tsx | 4 +- .../iteration-log/iteration-result-panel.tsx | 2 +- .../run/loop-log/loop-result-panel.tsx | 2 +- .../workflow/run/loop-result-panel.tsx | 2 +- web/app/forgot-password/page.tsx | 8 +- web/app/init/InitPasswordPopup.tsx | 2 + web/app/install/installForm.tsx | 2 + web/app/install/page.tsx | 7 +- web/app/layout.tsx | 11 +- web/app/reset-password/layout.tsx | 9 +- web/app/reset-password/page.tsx | 2 + web/app/signin/LoginLogo.tsx | 34 +++ web/app/signin/layout.tsx | 11 +- web/app/signin/normalForm.tsx | 71 +++--- web/context/access-control-store.ts | 34 +++ web/context/app-context.tsx | 11 +- web/context/global-public-context.tsx | 46 ++++ web/context/provider-context.tsx | 34 +++ web/eslint.config.mjs | 2 +- web/hooks/use-document-title.spec.ts | 65 ++++++ web/hooks/use-document-title.ts | 23 ++ web/hooks/use-tab-searchparams.ts | 8 +- web/i18n/de-DE/app-overview.ts | 22 +- web/i18n/de-DE/app.ts | 6 +- web/i18n/de-DE/custom.ts | 2 +- web/i18n/en-US/app-overview.ts | 22 +- web/i18n/en-US/app.ts | 41 +++- web/i18n/en-US/common.ts | 10 +- web/i18n/en-US/custom.ts | 2 +- web/i18n/en-US/explore.ts | 2 +- web/i18n/en-US/login.ts | 5 + web/i18n/es-ES/app-overview.ts | 8 +- web/i18n/es-ES/custom.ts | 2 +- web/i18n/fa-IR/app-overview.ts | 20 +- web/i18n/fa-IR/app.ts | 6 +- web/i18n/fr-FR/app-overview.ts | 12 +- web/i18n/fr-FR/app.ts | 6 +- web/i18n/fr-FR/custom.ts | 2 +- web/i18n/hi-IN/app-overview.ts | 6 +- web/i18n/hi-IN/app.ts | 4 +- web/i18n/hi-IN/custom.ts | 2 +- web/i18n/it-IT/app-overview.ts | 24 +- web/i18n/it-IT/app.ts | 6 +- web/i18n/it-IT/custom.ts | 2 +- web/i18n/ja-JP/app.ts | 37 +++- web/i18n/ja-JP/common.ts | 9 +- web/i18n/ja-JP/explore.ts | 2 +- web/i18n/ja-JP/login.ts | 5 + web/i18n/ko-KR/app-overview.ts | 2 +- web/i18n/ko-KR/app.ts | 6 +- web/i18n/ko-KR/custom.ts | 2 +- web/i18n/pl-PL/app-overview.ts | 10 +- web/i18n/pl-PL/app.ts | 2 +- web/i18n/pt-BR/app-overview.ts | 24 +- web/i18n/pt-BR/app.ts | 6 +- web/i18n/pt-BR/custom.ts | 2 +- web/i18n/ro-RO/app-overview.ts | 8 +- web/i18n/ro-RO/app.ts | 6 +- web/i18n/ro-RO/custom.ts | 2 +- web/i18n/ru-RU/app-overview.ts | 6 +- web/i18n/ru-RU/app.ts | 6 +- web/i18n/sl-SI/app.ts | 6 +- web/i18n/th-TH/app-overview.ts | 16 +- web/i18n/th-TH/app.ts | 6 +- web/i18n/th-TH/custom.ts | 2 +- web/i18n/tr-TR/app-overview.ts | 22 +- web/i18n/tr-TR/app.ts | 6 +- web/i18n/tr-TR/custom.ts | 2 +- web/i18n/uk-UA/app-overview.ts | 6 +- web/i18n/uk-UA/app.ts | 4 +- web/i18n/uk-UA/custom.ts | 2 +- web/i18n/vi-VN/app-overview.ts | 6 +- web/i18n/vi-VN/app.ts | 6 +- web/i18n/vi-VN/custom.ts | 2 +- web/i18n/zh-Hans/app-debug.ts | 2 +- web/i18n/zh-Hans/app-log.ts | 2 +- web/i18n/zh-Hans/app-overview.ts | 22 +- web/i18n/zh-Hans/app.ts | 46 +++- web/i18n/zh-Hans/common.ts | 10 +- web/i18n/zh-Hans/custom.ts | 2 +- web/i18n/zh-Hans/explore.ts | 2 +- web/i18n/zh-Hans/login.ts | 5 + web/i18n/zh-Hant/app-log.ts | 2 +- web/i18n/zh-Hant/app-overview.ts | 20 +- web/i18n/zh-Hant/app.ts | 6 +- web/i18n/zh-Hant/common.ts | 8 +- web/i18n/zh-Hant/custom.ts | 2 +- web/i18n/zh-Hant/explore.ts | 2 +- web/middleware.ts | 2 +- web/models/access-control.ts | 29 +++ web/models/app.ts | 61 +++++- web/service/access-control.ts | 90 ++++++++ web/service/apps.ts | 9 +- web/service/base.ts | 61 +++--- web/service/fetch.ts | 4 +- web/service/share.ts | 20 +- web/themes/manual-dark.css | 127 +++++------ web/themes/manual-light.css | 127 +++++------ web/types/app.ts | 3 + web/types/feature.ts | 34 ++- 199 files changed, 3618 insertions(+), 1000 deletions(-) create mode 100644 api/controllers/inner_api/mail.py create mode 100644 api/controllers/web/login.py create mode 100644 api/services/enterprise/mail_service.py create mode 100644 api/services/webapp_auth_service.py create mode 100644 api/tasks/mail_enterprise_task.py create mode 100644 api/templates/without-brand/email_code_login_mail_template_en-US.html create mode 100644 api/templates/without-brand/email_code_login_mail_template_zh-CN.html create mode 100644 api/templates/without-brand/invite_member_mail_template_en-US.html create mode 100644 api/templates/without-brand/invite_member_mail_template_zh-CN.html create mode 100644 api/templates/without-brand/reset_password_mail_template_en-US.html create mode 100644 api/templates/without-brand/reset_password_mail_template_zh-CN.html create mode 100644 web/app/(commonLayout)/apps/layout.tsx create mode 100644 web/app/components/app/app-access-control/access-control-dialog.tsx create mode 100644 web/app/components/app/app-access-control/access-control-item.tsx create mode 100644 web/app/components/app/app-access-control/add-member-or-group-pop.tsx create mode 100644 web/app/components/app/app-access-control/index.tsx create mode 100644 web/app/components/app/app-access-control/specific-groups-or-members.tsx create mode 100644 web/app/signin/LoginLogo.tsx create mode 100644 web/context/access-control-store.ts create mode 100644 web/context/global-public-context.tsx create mode 100644 web/hooks/use-document-title.spec.ts create mode 100644 web/hooks/use-document-title.ts create mode 100644 web/models/access-control.ts create mode 100644 web/service/access-control.ts diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 30c0ff000d..b06ab9653e 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -139,6 +139,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: + fetch-depth: 0 persist-credentials: false - name: Check changed files diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index f97209c369..860166a61a 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -17,15 +17,13 @@ from controllers.console.wraps import ( ) from core.ops.ops_trace_manager import OpsTraceManager from extensions.ext_database import db -from fields.app_fields import ( - app_detail_fields, - app_detail_fields_with_site, - app_pagination_fields, -) +from fields.app_fields import app_detail_fields, app_detail_fields_with_site, app_pagination_fields from libs.login import login_required from models import Account, App from services.app_dsl_service import AppDslService, ImportMode from services.app_service import AppService +from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"] @@ -75,7 +73,17 @@ class AppListApi(Resource): if not app_pagination: return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False} - return marshal(app_pagination, app_pagination_fields) + if FeatureService.get_system_features().webapp_auth.enabled: + app_ids = [str(app.id) for app in app_pagination.items] + res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids) + if len(res) != len(app_ids): + raise BadRequest("Invalid app id in webapp auth") + + for app in app_pagination.items: + if str(app.id) in res: + app.access_mode = res[str(app.id)].access_mode + + return marshal(app_pagination, app_pagination_fields), 200 @setup_required @login_required @@ -119,6 +127,10 @@ class AppApi(Resource): app_model = app_service.get_app(app_model) + if FeatureService.get_system_features().webapp_auth.enabled: + app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id)) + app_model.access_mode = app_setting.access_mode + return app_model @setup_required diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index d73d8ce701..8db19095d2 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -24,7 +24,7 @@ from libs.password import hash_password, valid_password from models.account import Account from services.account_service import AccountService, TenantService from services.errors.account import AccountRegisterError -from services.errors.workspace import WorkSpaceNotAllowedCreateError +from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError from services.feature_service import FeatureService @@ -119,6 +119,9 @@ class ForgotPasswordResetApi(Resource): if not reset_data: raise InvalidTokenError() # Must use token in reset phase + if reset_data.get("phase", "") != "reset": + raise InvalidTokenError() + # Must use token in reset phase if reset_data.get("phase", "") != "reset": raise InvalidTokenError() @@ -168,6 +171,8 @@ class ForgotPasswordResetApi(Resource): ) except WorkSpaceNotAllowedCreateError: pass + except WorkspacesLimitExceededError: + pass except AccountRegisterError: raise AccountInFreezeError() diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 27864bab3d..86231bf616 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -21,6 +21,7 @@ from controllers.console.error import ( AccountNotFound, EmailSendIpLimitError, NotAllowedCreateWorkspace, + WorkspacesLimitExceeded, ) from controllers.console.wraps import email_password_login_enabled, setup_required from events.tenant_event import tenant_was_created @@ -30,7 +31,7 @@ from models.account import Account from services.account_service import AccountService, RegisterService, TenantService from services.billing_service import BillingService from services.errors.account import AccountRegisterError -from services.errors.workspace import WorkSpaceNotAllowedCreateError +from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError from services.feature_service import FeatureService @@ -88,10 +89,15 @@ class LoginApi(Resource): # SELF_HOSTED only have one workspace tenants = TenantService.get_join_tenants(account) if len(tenants) == 0: - return { - "result": "fail", - "data": "workspace not found, please contact system admin to invite you to join in a workspace", - } + system_features = FeatureService.get_system_features() + + if system_features.is_allow_create_workspace and not system_features.license.workspaces.is_available(): + raise WorkspacesLimitExceeded() + else: + return { + "result": "fail", + "data": "workspace not found, please contact system admin to invite you to join in a workspace", + } token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) @@ -198,6 +204,9 @@ class EmailCodeLoginApi(Resource): if account: tenant = TenantService.get_join_tenants(account) if not tenant: + workspaces = FeatureService.get_system_features().license.workspaces + if not workspaces.is_available(): + raise WorkspacesLimitExceeded() if not FeatureService.get_system_features().is_allow_create_workspace: raise NotAllowedCreateWorkspace() else: @@ -215,6 +224,8 @@ class EmailCodeLoginApi(Resource): return NotAllowedCreateWorkspace() except AccountRegisterError as are: raise AccountInFreezeError() + except WorkspacesLimitExceededError: + raise WorkspacesLimitExceeded() token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token_pair.model_dump()} diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index b8fd1f0358..6944c56bf8 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -46,6 +46,18 @@ class NotAllowedCreateWorkspace(BaseHTTPException): code = 400 +class WorkspaceMembersLimitExceeded(BaseHTTPException): + error_code = "limit_exceeded" + description = "Unable to add member because the maximum workspace's member limit was exceeded" + code = 400 + + +class WorkspacesLimitExceeded(BaseHTTPException): + error_code = "limit_exceeded" + description = "Unable to create workspace because the maximum workspace limit was exceeded" + code = 400 + + class AccountBannedError(BaseHTTPException): error_code = "account_banned" description = "Account is banned." diff --git a/api/controllers/console/explore/error.py b/api/controllers/console/explore/error.py index 18221b7797..1e05ff4206 100644 --- a/api/controllers/console/explore/error.py +++ b/api/controllers/console/explore/error.py @@ -23,3 +23,9 @@ class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException): error_code = "app_suggested_questions_after_answer_disabled" description = "Function Suggested questions after answer disabled." code = 403 + + +class AppAccessDeniedError(BaseHTTPException): + error_code = "access_denied" + description = "App access denied." + code = 403 diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index 4062972d08..f73a226c89 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -1,3 +1,4 @@ +import logging from datetime import UTC, datetime from typing import Any @@ -15,6 +16,11 @@ from fields.installed_app_fields import installed_app_list_fields from libs.login import login_required from models import App, InstalledApp, RecommendedApp from services.account_service import TenantService +from services.app_service import AppService +from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService + +logger = logging.getLogger(__name__) class InstalledAppsListApi(Resource): @@ -48,6 +54,21 @@ class InstalledAppsListApi(Resource): for installed_app in installed_apps if installed_app.app is not None ] + + # filter out apps that user doesn't have access to + if FeatureService.get_system_features().webapp_auth.enabled: + user_id = current_user.id + res = [] + for installed_app in installed_app_list: + app_code = AppService.get_app_code_by_id(str(installed_app["app"].id)) + if EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( + user_id=user_id, + app_code=app_code, + ): + res.append(installed_app) + installed_app_list = res + logger.debug(f"installed_app_list: {installed_app_list}, user_id: {user_id}") + installed_app_list.sort( key=lambda app: ( -app["is_pinned"], diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py index 49ea81a8a0..afbd78bd5b 100644 --- a/api/controllers/console/explore/wraps.py +++ b/api/controllers/console/explore/wraps.py @@ -4,10 +4,14 @@ from flask_login import current_user from flask_restful import Resource from werkzeug.exceptions import NotFound +from controllers.console.explore.error import AppAccessDeniedError from controllers.console.wraps import account_initialization_required from extensions.ext_database import db from libs.login import login_required from models import InstalledApp +from services.app_service import AppService +from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService def installed_app_required(view=None): @@ -48,6 +52,36 @@ def installed_app_required(view=None): return decorator +def user_allowed_to_access_app(view=None): + def decorator(view): + @wraps(view) + def decorated(installed_app: InstalledApp, *args, **kwargs): + feature = FeatureService.get_system_features() + if feature.webapp_auth.enabled: + app_id = installed_app.app_id + app_code = AppService.get_app_code_by_id(app_id) + res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( + user_id=str(current_user.id), + app_code=app_code, + ) + if not res: + raise AppAccessDeniedError() + + return view(installed_app, *args, **kwargs) + + return decorated + + if view: + return decorator(view) + return decorator + + class InstalledAppResource(Resource): # must be reversed if there are multiple decorators - method_decorators = [installed_app_required, account_initialization_required, login_required] + + method_decorators = [ + user_allowed_to_access_app, + installed_app_required, + account_initialization_required, + login_required, + ] diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index a0031307fd..db49da7840 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -6,6 +6,7 @@ from flask_restful import Resource, abort, marshal_with, reqparse import services from configs import dify_config from controllers.console import api +from controllers.console.error import WorkspaceMembersLimitExceeded from controllers.console.wraps import ( account_initialization_required, cloud_edition_billing_resource_check, @@ -17,6 +18,7 @@ from libs.login import login_required from models.account import Account, TenantAccountRole from services.account_service import RegisterService, TenantService from services.errors.account import AccountAlreadyInTenantError +from services.feature_service import FeatureService class MemberListApi(Resource): @@ -54,6 +56,12 @@ class MemberInviteEmailApi(Resource): inviter = current_user invitation_results = [] console_web_url = dify_config.CONSOLE_WEB_URL + + workspace_members = FeatureService.get_features(tenant_id=inviter.current_tenant.id).workspace_members + + if not workspace_members.is_available(len(invitee_emails)): + raise WorkspaceMembersLimitExceeded() + for invitee_email in invitee_emails: try: token = RegisterService.invite_new_member( diff --git a/api/controllers/inner_api/__init__.py b/api/controllers/inner_api/__init__.py index f147a3453f..d51db4322a 100644 --- a/api/controllers/inner_api/__init__.py +++ b/api/controllers/inner_api/__init__.py @@ -5,5 +5,6 @@ from libs.external_api import ExternalApi bp = Blueprint("inner_api", __name__, url_prefix="/inner/api") api = ExternalApi(bp) +from . import mail from .plugin import plugin from .workspace import workspace diff --git a/api/controllers/inner_api/mail.py b/api/controllers/inner_api/mail.py new file mode 100644 index 0000000000..ce3373d65c --- /dev/null +++ b/api/controllers/inner_api/mail.py @@ -0,0 +1,27 @@ +from flask_restful import ( + Resource, # type: ignore + reqparse, +) + +from controllers.console.wraps import setup_required +from controllers.inner_api import api +from controllers.inner_api.wraps import enterprise_inner_api_only +from services.enterprise.mail_service import DifyMail, EnterpriseMailService + + +class EnterpriseMail(Resource): + @setup_required + @enterprise_inner_api_only + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("to", type=str, action="append", required=True) + parser.add_argument("subject", type=str, required=True) + parser.add_argument("body", type=str, required=True) + parser.add_argument("substitutions", type=dict, required=False) + args = parser.parse_args() + + EnterpriseMailService.send_mail(DifyMail(**args)) + return {"message": "success"}, 200 + + +api.add_resource(EnterpriseMail, "/enterprise/mail") diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index c9a37af5ed..bb4486bd91 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -1,12 +1,15 @@ -from flask_restful import marshal_with +from flask import request +from flask_restful import Resource, marshal_with, reqparse from controllers.common import fields from controllers.web import api from controllers.web.error import AppUnavailableError from controllers.web.wraps import WebApiResource from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict +from libs.passport import PassportService from models.model import App, AppMode from services.app_service import AppService +from services.enterprise.enterprise_service import EnterpriseService class AppParameterApi(WebApiResource): @@ -40,5 +43,51 @@ class AppMeta(WebApiResource): return AppService().get_app_meta(app_model) +class AppAccessMode(Resource): + def get(self): + parser = reqparse.RequestParser() + parser.add_argument("appId", type=str, required=True, location="args") + args = parser.parse_args() + + app_id = args["appId"] + res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id) + + return {"accessMode": res.access_mode} + + +class AppWebAuthPermission(Resource): + def get(self): + user_id = "visitor" + try: + auth_header = request.headers.get("Authorization") + if auth_header is None: + raise + if " " not in auth_header: + raise + + auth_scheme, tk = auth_header.split(None, 1) + auth_scheme = auth_scheme.lower() + if auth_scheme != "bearer": + raise + + decoded = PassportService().verify(tk) + user_id = decoded.get("user_id", "visitor") + except Exception as e: + pass + + parser = reqparse.RequestParser() + parser.add_argument("appId", type=str, required=True, location="args") + args = parser.parse_args() + + app_id = args["appId"] + app_code = AppService.get_app_code_by_id(app_id) + + res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code) + return {"result": res} + + api.add_resource(AppParameterApi, "/parameters") api.add_resource(AppMeta, "/meta") +# webapp auth apis +api.add_resource(AppAccessMode, "/webapp/access-mode") +api.add_resource(AppWebAuthPermission, "/webapp/permission") diff --git a/api/controllers/web/error.py b/api/controllers/web/error.py index 9fe5d08d54..4371e679db 100644 --- a/api/controllers/web/error.py +++ b/api/controllers/web/error.py @@ -121,9 +121,15 @@ class UnsupportedFileTypeError(BaseHTTPException): code = 415 -class WebSSOAuthRequiredError(BaseHTTPException): +class WebAppAuthRequiredError(BaseHTTPException): error_code = "web_sso_auth_required" - description = "Web SSO authentication required." + description = "Web app authentication required." + code = 401 + + +class WebAppAuthAccessDeniedError(BaseHTTPException): + error_code = "web_app_access_denied" + description = "You do not have permission to access this web app." code = 401 diff --git a/api/controllers/web/login.py b/api/controllers/web/login.py new file mode 100644 index 0000000000..06c2274440 --- /dev/null +++ b/api/controllers/web/login.py @@ -0,0 +1,120 @@ +from flask import request +from flask_restful import Resource, reqparse +from jwt import InvalidTokenError # type: ignore +from werkzeug.exceptions import BadRequest + +import services +from controllers.console.auth.error import EmailCodeError, EmailOrPasswordMismatchError, InvalidEmailError +from controllers.console.error import AccountBannedError, AccountNotFound +from controllers.console.wraps import setup_required +from libs.helper import email +from libs.password import valid_password +from services.account_service import AccountService +from services.webapp_auth_service import WebAppAuthService + + +class LoginApi(Resource): + """Resource for web app email/password login.""" + + def post(self): + """Authenticate user and login.""" + parser = reqparse.RequestParser() + parser.add_argument("email", type=email, required=True, location="json") + parser.add_argument("password", type=valid_password, required=True, location="json") + args = parser.parse_args() + + app_code = request.headers.get("X-App-Code") + if app_code is None: + raise BadRequest("X-App-Code header is missing.") + + try: + account = WebAppAuthService.authenticate(args["email"], args["password"]) + except services.errors.account.AccountLoginError: + raise AccountBannedError() + except services.errors.account.AccountPasswordError: + raise EmailOrPasswordMismatchError() + except services.errors.account.AccountNotFoundError: + raise AccountNotFound() + + WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code) + + end_user = WebAppAuthService.create_end_user(email=args["email"], app_code=app_code) + + token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id) + return {"result": "success", "token": token} + + +# class LogoutApi(Resource): +# @setup_required +# def get(self): +# account = cast(Account, flask_login.current_user) +# if isinstance(account, flask_login.AnonymousUserMixin): +# return {"result": "success"} +# flask_login.logout_user() +# return {"result": "success"} + + +class EmailCodeLoginSendEmailApi(Resource): + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=email, required=True, location="json") + parser.add_argument("language", type=str, required=False, location="json") + args = parser.parse_args() + + if args["language"] is not None and args["language"] == "zh-Hans": + language = "zh-Hans" + else: + language = "en-US" + + account = WebAppAuthService.get_user_through_email(args["email"]) + if account is None: + raise AccountNotFound() + else: + token = WebAppAuthService.send_email_code_login_email(account=account, language=language) + + return {"result": "success", "data": token} + + +class EmailCodeLoginApi(Resource): + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=str, required=True, location="json") + parser.add_argument("code", type=str, required=True, location="json") + parser.add_argument("token", type=str, required=True, location="json") + args = parser.parse_args() + + user_email = args["email"] + app_code = request.headers.get("X-App-Code") + if app_code is None: + raise BadRequest("X-App-Code header is missing.") + + token_data = WebAppAuthService.get_email_code_login_data(args["token"]) + if token_data is None: + raise InvalidTokenError() + + if token_data["email"] != args["email"]: + raise InvalidEmailError() + + if token_data["code"] != args["code"]: + raise EmailCodeError() + + WebAppAuthService.revoke_email_code_login_token(args["token"]) + account = WebAppAuthService.get_user_through_email(user_email) + if not account: + raise AccountNotFound() + + WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code) + + end_user = WebAppAuthService.create_end_user(email=user_email, app_code=app_code) + + token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id) + AccountService.reset_login_error_rate_limit(args["email"]) + return {"result": "success", "token": token} + + +# api.add_resource(LoginApi, "/login") +# api.add_resource(LogoutApi, "/logout") +# api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") +# api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py index 267dac223d..154c6772b7 100644 --- a/api/controllers/web/passport.py +++ b/api/controllers/web/passport.py @@ -5,7 +5,7 @@ from flask_restful import Resource from werkzeug.exceptions import NotFound, Unauthorized from controllers.web import api -from controllers.web.error import WebSSOAuthRequiredError +from controllers.web.error import WebAppAuthRequiredError from extensions.ext_database import db from libs.passport import PassportService from models.model import App, EndUser, Site @@ -24,10 +24,10 @@ class PassportResource(Resource): if app_code is None: raise Unauthorized("X-App-Code header is missing.") - if system_features.sso_enforced_for_web: - app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False) - if app_web_sso_enabled: - raise WebSSOAuthRequiredError() + if system_features.webapp_auth.enabled: + app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) + if not app_settings or not app_settings.access_mode == "public": + raise WebAppAuthRequiredError() # get site from db and check if it is normal site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first() diff --git a/api/controllers/web/wraps.py b/api/controllers/web/wraps.py index c327c3df18..3bb029d6eb 100644 --- a/api/controllers/web/wraps.py +++ b/api/controllers/web/wraps.py @@ -4,7 +4,7 @@ from flask import request from flask_restful import Resource from werkzeug.exceptions import BadRequest, NotFound, Unauthorized -from controllers.web.error import WebSSOAuthRequiredError +from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequiredError from extensions.ext_database import db from libs.passport import PassportService from models.model import App, EndUser, Site @@ -29,7 +29,7 @@ def validate_jwt_token(view=None): def decode_jwt_token(): system_features = FeatureService.get_system_features() - app_code = request.headers.get("X-App-Code") + app_code = str(request.headers.get("X-App-Code")) try: auth_header = request.headers.get("Authorization") if auth_header is None: @@ -57,35 +57,53 @@ def decode_jwt_token(): if not end_user: raise NotFound() - _validate_web_sso_token(decoded, system_features, app_code) + # for enterprise webapp auth + app_web_auth_enabled = False + if system_features.webapp_auth.enabled: + app_web_auth_enabled = ( + EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public" + ) + + _validate_webapp_token(decoded, app_web_auth_enabled, system_features.webapp_auth.enabled) + _validate_user_accessibility(decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled) return app_model, end_user except Unauthorized as e: - if system_features.sso_enforced_for_web: - app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False) - if app_web_sso_enabled: - raise WebSSOAuthRequiredError() + if system_features.webapp_auth.enabled: + app_web_auth_enabled = ( + EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=str(app_code)).access_mode != "public" + ) + if app_web_auth_enabled: + raise WebAppAuthRequiredError() raise Unauthorized(e.description) -def _validate_web_sso_token(decoded, system_features, app_code): - app_web_sso_enabled = False - - # Check if SSO is enforced for web, and if the token source is not SSO, raise an error and redirect to SSO login - if system_features.sso_enforced_for_web: - app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False) - if app_web_sso_enabled: - source = decoded.get("token_source") - if not source or source != "sso": - raise WebSSOAuthRequiredError() - - # Check if SSO is not enforced for web, and if the token source is SSO, - # raise an error and redirect to normal passport login - if not system_features.sso_enforced_for_web or not app_web_sso_enabled: +def _validate_webapp_token(decoded, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool): + # Check if authentication is enforced for web app, and if the token source is not webapp, + # raise an error and redirect to login + if system_webapp_auth_enabled and app_web_auth_enabled: source = decoded.get("token_source") - if source and source == "sso": - raise Unauthorized("sso token expired.") + if not source or source != "webapp": + raise WebAppAuthRequiredError() + + # Check if authentication is not enforced for web, and if the token source is webapp, + # raise an error and redirect to normal passport login + if not system_webapp_auth_enabled or not app_web_auth_enabled: + source = decoded.get("token_source") + if source and source == "webapp": + raise Unauthorized("webapp token expired.") + + +def _validate_user_accessibility(decoded, app_code, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool): + if system_webapp_auth_enabled and app_web_auth_enabled: + # Check if the user is allowed to access the web app + user_id = decoded.get("user_id") + if not user_id: + raise WebAppAuthRequiredError() + + if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code): + raise WebAppAuthAccessDeniedError() class WebApiResource(Resource): diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index 0b0e2a2f54..416870cafa 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -63,6 +63,7 @@ app_detail_fields = { "created_at": TimestampField, "updated_by": fields.String, "updated_at": TimestampField, + "access_mode": fields.String, } prompt_config_fields = { @@ -98,6 +99,7 @@ app_partial_fields = { "updated_by": fields.String, "updated_at": TimestampField, "tags": fields.List(fields.Nested(tag_fields)), + "access_mode": fields.String, } @@ -176,6 +178,7 @@ app_detail_fields_with_site = { "updated_by": fields.String, "updated_at": TimestampField, "deleted_tools": fields.List(fields.Nested(deleted_tool_fields)), + "access_mode": fields.String, } diff --git a/api/services/account_service.py b/api/services/account_service.py index d21926d746..ac84a46299 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -49,7 +49,7 @@ from services.errors.account import ( RoleAlreadyAssignedError, TenantNotFoundError, ) -from services.errors.workspace import WorkSpaceNotAllowedCreateError +from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError from services.feature_service import FeatureService from tasks.delete_account_task import delete_account_task from tasks.mail_account_deletion_task import send_account_deletion_verification_code @@ -628,6 +628,10 @@ class TenantService: if not FeatureService.get_system_features().is_allow_create_workspace and not is_setup: raise WorkSpaceNotAllowedCreateError() + workspaces = FeatureService.get_system_features().license.workspaces + if not workspaces.is_available(): + raise WorkspacesLimitExceededError() + if name: tenant = TenantService.create_tenant(name=name, is_setup=is_setup) else: @@ -928,7 +932,11 @@ class RegisterService: if open_id is not None and provider is not None: AccountService.link_account_integrate(provider, open_id, account) - if FeatureService.get_system_features().is_allow_create_workspace and create_workspace_required: + if ( + FeatureService.get_system_features().is_allow_create_workspace + and create_workspace_required + and FeatureService.get_system_features().license.workspaces.is_available() + ): tenant = TenantService.create_tenant(f"{account.name}'s Workspace") TenantService.create_tenant_member(tenant, account, role="owner") account.current_tenant = tenant diff --git a/api/services/app_service.py b/api/services/app_service.py index 2fae479e05..ebebf8fa58 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -18,8 +18,10 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_was_created from extensions.ext_database import db from models.account import Account -from models.model import App, AppMode, AppModelConfig +from models.model import App, AppMode, AppModelConfig, Site from models.tools import ApiToolProvider +from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService from services.tag_service import TagService from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task @@ -155,6 +157,10 @@ class AppService: app_was_created.send(app, account=account) + if FeatureService.get_system_features().webapp_auth.enabled: + # update web app setting as private + EnterpriseService.WebAppAuth.update_app_access_mode(app.id, "private") + return app def get_app(self, app: App) -> App: @@ -307,6 +313,10 @@ class AppService: db.session.delete(app) db.session.commit() + # clean up web app settings + if FeatureService.get_system_features().webapp_auth.enabled: + EnterpriseService.WebAppAuth.cleanup_webapp(app.id) + # Trigger asynchronous deletion of app and related data remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id) @@ -373,3 +383,15 @@ class AppService: meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"} return meta + + @staticmethod + def get_app_code_by_id(app_id: str) -> str: + """ + Get app code by app id + :param app_id: app id + :return: app code + """ + site = db.session.query(Site).filter(Site.app_id == app_id).first() + if not site: + raise ValueError(f"App with id {app_id} not found") + return str(site.code) diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py index abc01ddf8f..1be78d2e62 100644 --- a/api/services/enterprise/enterprise_service.py +++ b/api/services/enterprise/enterprise_service.py @@ -1,11 +1,90 @@ +from pydantic import BaseModel, Field + from services.enterprise.base import EnterpriseRequest +class WebAppSettings(BaseModel): + access_mode: str = Field( + description="Access mode for the web app. Can be 'public' or 'private'", + default="private", + alias="accessMode", + ) + + class EnterpriseService: @classmethod def get_info(cls): return EnterpriseRequest.send_request("GET", "/info") @classmethod - def get_app_web_sso_enabled(cls, app_code): - return EnterpriseRequest.send_request("GET", f"/app-sso-setting?appCode={app_code}") + def get_workspace_info(cls, tenant_id: str): + return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info") + + class WebAppAuth: + @classmethod + def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str): + params = {"userId": user_id, "appCode": app_code} + data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params) + + return data.get("result", False) + + @classmethod + def get_app_access_mode_by_id(cls, app_id: str) -> WebAppSettings: + if not app_id: + raise ValueError("app_id must be provided.") + params = {"appId": app_id} + data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/id", params=params) + if not data: + raise ValueError("No data found.") + return WebAppSettings(**data) + + @classmethod + def batch_get_app_access_mode_by_id(cls, app_ids: list[str]) -> dict[str, WebAppSettings]: + if not app_ids: + return {} + body = {"appIds": app_ids} + data: dict[str, str] = EnterpriseRequest.send_request("POST", "/webapp/access-mode/batch/id", json=body) + if not data: + raise ValueError("No data found.") + + if not isinstance(data["accessModes"], dict): + raise ValueError("Invalid data format.") + + ret = {} + for key, value in data["accessModes"].items(): + curr = WebAppSettings() + curr.access_mode = value + ret[key] = curr + + return ret + + @classmethod + def get_app_access_mode_by_code(cls, app_code: str) -> WebAppSettings: + if not app_code: + raise ValueError("app_code must be provided.") + params = {"appCode": app_code} + data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/code", params=params) + if not data: + raise ValueError("No data found.") + return WebAppSettings(**data) + + @classmethod + def update_app_access_mode(cls, app_id: str, access_mode: str): + if not app_id: + raise ValueError("app_id must be provided.") + if access_mode not in ["public", "private", "private_all"]: + raise ValueError("access_mode must be either 'public', 'private', or 'private_all'") + + data = {"appId": app_id, "accessMode": access_mode} + + response = EnterpriseRequest.send_request("POST", "/webapp/access-mode", json=data) + + return response.get("result", False) + + @classmethod + def cleanup_webapp(cls, app_id: str): + if not app_id: + raise ValueError("app_id must be provided.") + + body = {"appId": app_id} + EnterpriseRequest.send_request("DELETE", "/webapp/clean", json=body) diff --git a/api/services/enterprise/mail_service.py b/api/services/enterprise/mail_service.py new file mode 100644 index 0000000000..630e7679ac --- /dev/null +++ b/api/services/enterprise/mail_service.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +from tasks.mail_enterprise_task import send_enterprise_email_task + + +class DifyMail(BaseModel): + to: list[str] + subject: str + body: str + substitutions: dict[str, str] = {} + + +class EnterpriseMailService: + @classmethod + def send_mail(cls, mail: DifyMail): + send_enterprise_email_task.delay( + to=mail.to, subject=mail.subject, body=mail.body, substitutions=mail.substitutions + ) diff --git a/api/services/errors/workspace.py b/api/services/errors/workspace.py index 714064ffdf..577238507f 100644 --- a/api/services/errors/workspace.py +++ b/api/services/errors/workspace.py @@ -7,3 +7,7 @@ class WorkSpaceNotAllowedCreateError(BaseServiceError): class WorkSpaceNotFoundError(BaseServiceError): pass + + +class WorkspacesLimitExceededError(BaseServiceError): + pass diff --git a/api/services/feature_service.py b/api/services/feature_service.py index c2226c319f..be85a03e80 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -1,6 +1,6 @@ from enum import StrEnum -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from configs import dify_config from services.billing_service import BillingService @@ -27,6 +27,32 @@ class LimitationModel(BaseModel): limit: int = 0 +class LicenseLimitationModel(BaseModel): + """ + - enabled: whether this limit is enforced + - size: current usage count + - limit: maximum allowed count; 0 means unlimited + """ + + enabled: bool = Field(False, description="Whether this limit is currently active") + size: int = Field(0, description="Number of resources already consumed") + limit: int = Field(0, description="Maximum number of resources allowed; 0 means no limit") + + def is_available(self, required: int = 1) -> bool: + """ + Determine whether the requested amount can be allocated. + + Returns True if: + - this limit is not active, or + - the limit is zero (unlimited), or + - there is enough remaining quota. + """ + if not self.enabled or self.limit == 0: + return True + + return (self.limit - self.size) >= required + + class LicenseStatus(StrEnum): NONE = "none" INACTIVE = "inactive" @@ -39,6 +65,27 @@ class LicenseStatus(StrEnum): class LicenseModel(BaseModel): status: LicenseStatus = LicenseStatus.NONE expired_at: str = "" + workspaces: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0) + + +class BrandingModel(BaseModel): + enabled: bool = False + application_title: str = "" + login_page_logo: str = "" + workspace_logo: str = "" + favicon: str = "" + + +class WebAppAuthSSOModel(BaseModel): + protocol: str = "" + + +class WebAppAuthModel(BaseModel): + enabled: bool = False + allow_sso: bool = False + sso_config: WebAppAuthSSOModel = WebAppAuthSSOModel() + allow_email_code_login: bool = False + allow_email_password_login: bool = False class FeatureModel(BaseModel): @@ -54,6 +101,8 @@ class FeatureModel(BaseModel): can_replace_logo: bool = False model_load_balancing_enabled: bool = False dataset_operator_enabled: bool = False + webapp_copyright_enabled: bool = False + workspace_members: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0) # pydantic configs model_config = ConfigDict(protected_namespaces=()) @@ -68,9 +117,6 @@ class KnowledgeRateLimitModel(BaseModel): class SystemFeatureModel(BaseModel): sso_enforced_for_signin: bool = False sso_enforced_for_signin_protocol: str = "" - sso_enforced_for_web: bool = False - sso_enforced_for_web_protocol: str = "" - enable_web_sso_switch_component: bool = False enable_marketplace: bool = False max_plugin_package_size: int = dify_config.PLUGIN_MAX_PACKAGE_SIZE enable_email_code_login: bool = False @@ -80,6 +126,8 @@ class SystemFeatureModel(BaseModel): is_allow_create_workspace: bool = False is_email_setup: bool = False license: LicenseModel = LicenseModel() + branding: BrandingModel = BrandingModel() + webapp_auth: WebAppAuthModel = WebAppAuthModel() class FeatureService: @@ -92,6 +140,10 @@ class FeatureService: if dify_config.BILLING_ENABLED and tenant_id: cls._fulfill_params_from_billing_api(features, tenant_id) + if dify_config.ENTERPRISE_ENABLED: + features.webapp_copyright_enabled = True + cls._fulfill_params_from_workspace_info(features, tenant_id) + return features @classmethod @@ -111,8 +163,8 @@ class FeatureService: cls._fulfill_system_params_from_env(system_features) if dify_config.ENTERPRISE_ENABLED: - system_features.enable_web_sso_switch_component = True - + system_features.branding.enabled = True + system_features.webapp_auth.enabled = True cls._fulfill_params_from_enterprise(system_features) if dify_config.MARKETPLACE_ENABLED: @@ -136,6 +188,14 @@ class FeatureService: features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED features.education.enabled = dify_config.EDUCATION_ENABLED + @classmethod + def _fulfill_params_from_workspace_info(cls, features: FeatureModel, tenant_id: str): + workspace_info = EnterpriseService.get_workspace_info(tenant_id) + if "WorkspaceMembers" in workspace_info: + features.workspace_members.size = workspace_info["WorkspaceMembers"]["used"] + features.workspace_members.limit = workspace_info["WorkspaceMembers"]["limit"] + features.workspace_members.enabled = workspace_info["WorkspaceMembers"]["enabled"] + @classmethod def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str): billing_info = BillingService.get_info(tenant_id) @@ -145,6 +205,9 @@ class FeatureService: features.billing.subscription.interval = billing_info["subscription"]["interval"] features.education.activated = billing_info["subscription"].get("education", False) + if features.billing.subscription.plan != "sandbox": + features.webapp_copyright_enabled = True + if "members" in billing_info: features.members.size = billing_info["members"]["size"] features.members.limit = billing_info["members"]["limit"] @@ -178,38 +241,53 @@ class FeatureService: features.knowledge_rate_limit = billing_info["knowledge_rate_limit"]["limit"] @classmethod - def _fulfill_params_from_enterprise(cls, features): + def _fulfill_params_from_enterprise(cls, features: SystemFeatureModel): enterprise_info = EnterpriseService.get_info() - if "sso_enforced_for_signin" in enterprise_info: - features.sso_enforced_for_signin = enterprise_info["sso_enforced_for_signin"] + if "SSOEnforcedForSignin" in enterprise_info: + features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"] - if "sso_enforced_for_signin_protocol" in enterprise_info: - features.sso_enforced_for_signin_protocol = enterprise_info["sso_enforced_for_signin_protocol"] + if "SSOEnforcedForSigninProtocol" in enterprise_info: + features.sso_enforced_for_signin_protocol = enterprise_info["SSOEnforcedForSigninProtocol"] - if "sso_enforced_for_web" in enterprise_info: - features.sso_enforced_for_web = enterprise_info["sso_enforced_for_web"] + if "EnableEmailCodeLogin" in enterprise_info: + features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"] - if "sso_enforced_for_web_protocol" in enterprise_info: - features.sso_enforced_for_web_protocol = enterprise_info["sso_enforced_for_web_protocol"] + if "EnableEmailPasswordLogin" in enterprise_info: + features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"] - if "enable_email_code_login" in enterprise_info: - features.enable_email_code_login = enterprise_info["enable_email_code_login"] + if "IsAllowRegister" in enterprise_info: + features.is_allow_register = enterprise_info["IsAllowRegister"] - if "enable_email_password_login" in enterprise_info: - features.enable_email_password_login = enterprise_info["enable_email_password_login"] + if "IsAllowCreateWorkspace" in enterprise_info: + features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"] - if "is_allow_register" in enterprise_info: - features.is_allow_register = enterprise_info["is_allow_register"] + if "Branding" in enterprise_info: + features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "") + features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "") + features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "") + features.branding.favicon = enterprise_info["Branding"].get("favicon", "") - if "is_allow_create_workspace" in enterprise_info: - features.is_allow_create_workspace = enterprise_info["is_allow_create_workspace"] + if "WebAppAuth" in enterprise_info: + features.webapp_auth.allow_sso = enterprise_info["WebAppAuth"].get("allowSso", False) + features.webapp_auth.allow_email_code_login = enterprise_info["WebAppAuth"].get( + "allowEmailCodeLogin", False + ) + features.webapp_auth.allow_email_password_login = enterprise_info["WebAppAuth"].get( + "allowEmailPasswordLogin", False + ) + features.webapp_auth.sso_config.protocol = enterprise_info.get("SSOEnforcedForWebProtocol", "") - if "license" in enterprise_info: - license_info = enterprise_info["license"] + if "License" in enterprise_info: + license_info = enterprise_info["License"] if "status" in license_info: features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE)) - if "expired_at" in license_info: - features.license.expired_at = license_info["expired_at"] + if "expiredAt" in license_info: + features.license.expired_at = license_info["expiredAt"] + + if "workspaces" in license_info: + features.license.workspaces.enabled = license_info["workspaces"]["enabled"] + features.license.workspaces.limit = license_info["workspaces"]["limit"] + features.license.workspaces.size = license_info["workspaces"]["used"] diff --git a/api/services/webapp_auth_service.py b/api/services/webapp_auth_service.py new file mode 100644 index 0000000000..79d5217de7 --- /dev/null +++ b/api/services/webapp_auth_service.py @@ -0,0 +1,141 @@ +import random +from datetime import UTC, datetime, timedelta +from typing import Any, Optional, cast + +from werkzeug.exceptions import NotFound, Unauthorized + +from configs import dify_config +from controllers.web.error import WebAppAuthAccessDeniedError +from extensions.ext_database import db +from libs.helper import TokenManager +from libs.passport import PassportService +from libs.password import compare_password +from models.account import Account, AccountStatus +from models.model import App, EndUser, Site +from services.enterprise.enterprise_service import EnterpriseService +from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError +from services.feature_service import FeatureService +from tasks.mail_email_code_login import send_email_code_login_mail_task + + +class WebAppAuthService: + """Service for web app authentication.""" + + @staticmethod + def authenticate(email: str, password: str) -> Account: + """authenticate account with email and password""" + + account = Account.query.filter_by(email=email).first() + if not account: + raise AccountNotFoundError() + + if account.status == AccountStatus.BANNED.value: + raise AccountLoginError("Account is banned.") + + if account.password is None or not compare_password(password, account.password, account.password_salt): + raise AccountPasswordError("Invalid email or password.") + + return cast(Account, account) + + @classmethod + def login(cls, account: Account, app_code: str, end_user_id: str) -> str: + site = db.session.query(Site).filter(Site.code == app_code).first() + if not site: + raise NotFound("Site not found.") + + access_token = cls._get_account_jwt_token(account=account, site=site, end_user_id=end_user_id) + + return access_token + + @classmethod + def get_user_through_email(cls, email: str): + account = db.session.query(Account).filter(Account.email == email).first() + if not account: + return None + + if account.status == AccountStatus.BANNED.value: + raise Unauthorized("Account is banned.") + + return account + + @classmethod + def send_email_code_login_email( + cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" + ): + email = account.email if account else email + if email is None: + raise ValueError("Email must be provided.") + + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + token = TokenManager.generate_token( + account=account, email=email, token_type="webapp_email_code_login", additional_data={"code": code} + ) + send_email_code_login_mail_task.delay( + language=language, + to=account.email if account else email, + code=code, + ) + + return token + + @classmethod + def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]: + return TokenManager.get_token_data(token, "webapp_email_code_login") + + @classmethod + def revoke_email_code_login_token(cls, token: str): + TokenManager.revoke_token(token, "webapp_email_code_login") + + @classmethod + def create_end_user(cls, app_code, email) -> EndUser: + site = db.session.query(Site).filter(Site.code == app_code).first() + if not site: + raise NotFound("Site not found.") + app_model = db.session.query(App).filter(App.id == site.app_id).first() + if not app_model: + raise NotFound("App not found.") + end_user = EndUser( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + type="browser", + is_anonymous=False, + session_id=email, + name="enterpriseuser", + external_user_id="enterpriseuser", + ) + db.session.add(end_user) + db.session.commit() + + return end_user + + @classmethod + def _validate_user_accessibility(cls, account: Account, app_code: str): + """Check if the user is allowed to access the app.""" + system_features = FeatureService.get_system_features() + if system_features.webapp_auth.enabled: + app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) + + if ( + app_settings.access_mode != "public" + and not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(account.id, app_code=app_code) + ): + raise WebAppAuthAccessDeniedError() + + @classmethod + def _get_account_jwt_token(cls, account: Account, site: Site, end_user_id: str) -> str: + exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24) + exp = int(exp_dt.timestamp()) + + payload = { + "iss": site.id, + "sub": "Web API Passport", + "app_id": site.app_id, + "app_code": site.code, + "user_id": account.id, + "end_user_id": end_user_id, + "token_source": "webapp", + "exp": exp, + } + + token: str = PassportService().issue(payload) + return token diff --git a/api/tasks/mail_email_code_login.py b/api/tasks/mail_email_code_login.py index 5dc935548f..ddad331725 100644 --- a/api/tasks/mail_email_code_login.py +++ b/api/tasks/mail_email_code_login.py @@ -6,6 +6,7 @@ from celery import shared_task # type: ignore from flask import render_template from extensions.ext_mail import mail +from services.feature_service import FeatureService @shared_task(queue="mail") @@ -25,10 +26,24 @@ def send_email_code_login_mail_task(language: str, to: str, code: str): # send email code login mail using different languages try: if language == "zh-Hans": - html_content = render_template("email_code_login_mail_template_zh-CN.html", to=to, code=code) + template = "email_code_login_mail_template_zh-CN.html" + system_features = FeatureService.get_system_features() + if system_features.branding.enabled: + application_title = system_features.branding.application_title + template = "without-brand/email_code_login_mail_template_zh-CN.html" + html_content = render_template(template, to=to, code=code, application_title=application_title) + else: + html_content = render_template(template, to=to, code=code) mail.send(to=to, subject="邮箱验证码", html=html_content) else: - html_content = render_template("email_code_login_mail_template_en-US.html", to=to, code=code) + template = "email_code_login_mail_template_en-US.html" + system_features = FeatureService.get_system_features() + if system_features.branding.enabled: + application_title = system_features.branding.application_title + template = "without-brand/email_code_login_mail_template_en-US.html" + html_content = render_template(template, to=to, code=code, application_title=application_title) + else: + html_content = render_template(template, to=to, code=code) mail.send(to=to, subject="Email Code", html=html_content) end_at = time.perf_counter() diff --git a/api/tasks/mail_enterprise_task.py b/api/tasks/mail_enterprise_task.py new file mode 100644 index 0000000000..b9d8fd55df --- /dev/null +++ b/api/tasks/mail_enterprise_task.py @@ -0,0 +1,33 @@ +import logging +import time + +import click +from celery import shared_task # type: ignore +from flask import render_template_string + +from extensions.ext_mail import mail + + +@shared_task(queue="mail") +def send_enterprise_email_task(to, subject, body, substitutions): + if not mail.is_inited(): + return + + logging.info(click.style("Start enterprise mail to {} with subject {}".format(to, subject), fg="green")) + start_at = time.perf_counter() + + try: + html_content = render_template_string(body, **substitutions) + + if isinstance(to, list): + for t in to: + mail.send(to=t, subject=subject, html=html_content) + else: + mail.send(to=to, subject=subject, html=html_content) + + end_at = time.perf_counter() + logging.info( + click.style("Send enterprise mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green") + ) + except Exception: + logging.exception("Send enterprise mail to {} failed".format(to)) diff --git a/api/tasks/mail_invite_member_task.py b/api/tasks/mail_invite_member_task.py index 3094527fd4..7ca85c7f2d 100644 --- a/api/tasks/mail_invite_member_task.py +++ b/api/tasks/mail_invite_member_task.py @@ -7,6 +7,7 @@ from flask import render_template from configs import dify_config from extensions.ext_mail import mail +from services.feature_service import FeatureService @shared_task(queue="mail") @@ -33,23 +34,45 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam try: url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}" if language == "zh-Hans": - html_content = render_template( - "invite_member_mail_template_zh-CN.html", - to=to, - inviter_name=inviter_name, - workspace_name=workspace_name, - url=url, - ) - mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content) + template = "invite_member_mail_template_zh-CN.html" + system_features = FeatureService.get_system_features() + if system_features.branding.enabled: + application_title = system_features.branding.application_title + template = "without-brand/invite_member_mail_template_zh-CN.html" + html_content = render_template( + template, + to=to, + inviter_name=inviter_name, + workspace_name=workspace_name, + url=url, + application_title=application_title, + ) + mail.send(to=to, subject=f"立即加入 {application_title} 工作空间", html=html_content) + else: + html_content = render_template( + template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url + ) + mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content) else: - html_content = render_template( - "invite_member_mail_template_en-US.html", - to=to, - inviter_name=inviter_name, - workspace_name=workspace_name, - url=url, - ) - mail.send(to=to, subject="Join Dify Workspace Now", html=html_content) + template = "invite_member_mail_template_en-US.html" + system_features = FeatureService.get_system_features() + if system_features.branding.enabled: + application_title = system_features.branding.application_title + template = "without-brand/invite_member_mail_template_en-US.html" + html_content = render_template( + template, + to=to, + inviter_name=inviter_name, + workspace_name=workspace_name, + url=url, + application_title=application_title, + ) + mail.send(to=to, subject=f"Join {application_title} Workspace Now", html=html_content) + else: + html_content = render_template( + template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url + ) + mail.send(to=to, subject="Join Dify Workspace Now", html=html_content) end_at = time.perf_counter() logging.info( diff --git a/api/tasks/mail_reset_password_task.py b/api/tasks/mail_reset_password_task.py index d5be94431b..d4f4482a48 100644 --- a/api/tasks/mail_reset_password_task.py +++ b/api/tasks/mail_reset_password_task.py @@ -6,6 +6,7 @@ from celery import shared_task # type: ignore from flask import render_template from extensions.ext_mail import mail +from services.feature_service import FeatureService @shared_task(queue="mail") @@ -25,11 +26,27 @@ def send_reset_password_mail_task(language: str, to: str, code: str): # send reset password mail using different languages try: if language == "zh-Hans": - html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code) - mail.send(to=to, subject="设置您的 Dify 密码", html=html_content) + template = "reset_password_mail_template_zh-CN.html" + system_features = FeatureService.get_system_features() + if system_features.branding.enabled: + application_title = system_features.branding.application_title + template = "without-brand/reset_password_mail_template_zh-CN.html" + html_content = render_template(template, to=to, code=code, application_title=application_title) + mail.send(to=to, subject=f"设置您的 {application_title} 密码", html=html_content) + else: + html_content = render_template(template, to=to, code=code) + mail.send(to=to, subject="设置您的 Dify 密码", html=html_content) else: - html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code) - mail.send(to=to, subject="Set Your Dify Password", html=html_content) + template = "reset_password_mail_template_en-US.html" + system_features = FeatureService.get_system_features() + if system_features.branding.enabled: + application_title = system_features.branding.application_title + template = "without-brand/reset_password_mail_template_en-US.html" + html_content = render_template(template, to=to, code=code, application_title=application_title) + mail.send(to=to, subject=f"Set Your {application_title} Password", html=html_content) + else: + html_content = render_template(template, to=to, code=code) + mail.send(to=to, subject="Set Your Dify Password", html=html_content) end_at = time.perf_counter() logging.info( diff --git a/api/templates/without-brand/email_code_login_mail_template_en-US.html b/api/templates/without-brand/email_code_login_mail_template_en-US.html new file mode 100644 index 0000000000..63f34ff1d3 --- /dev/null +++ b/api/templates/without-brand/email_code_login_mail_template_en-US.html @@ -0,0 +1,70 @@ + + + + + + +
+

Your login code for {{application_title}}

+

Copy and paste this code, this code will only be valid for the next 5 minutes.

+
+ {{code}} +
+

If you didn't request a login, don't worry. You can safely ignore this email.

+
+ + diff --git a/api/templates/without-brand/email_code_login_mail_template_zh-CN.html b/api/templates/without-brand/email_code_login_mail_template_zh-CN.html new file mode 100644 index 0000000000..63d2a2ec61 --- /dev/null +++ b/api/templates/without-brand/email_code_login_mail_template_zh-CN.html @@ -0,0 +1,70 @@ + + + + + + +
+

{{application_title}} 的登录验证码

+

复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。

+
+ {{code}} +
+

如果您没有请求登录,请不要担心。您可以安全地忽略此电子邮件。

+
+ + diff --git a/api/templates/without-brand/invite_member_mail_template_en-US.html b/api/templates/without-brand/invite_member_mail_template_en-US.html new file mode 100644 index 0000000000..45f2ea292c --- /dev/null +++ b/api/templates/without-brand/invite_member_mail_template_en-US.html @@ -0,0 +1,69 @@ + + + + + + +
+
+

Dear {{ to }},

+

{{ inviter_name }} is pleased to invite you to join our workspace on {{application_title}}, a platform specifically designed for LLM application development. On {{application_title}}, you can explore, create, and collaborate to build and operate AI applications.

+

Click the button below to log in to {{application_title}} and join the workspace.

+

Login Here

+
+ +
+ + + diff --git a/api/templates/without-brand/invite_member_mail_template_zh-CN.html b/api/templates/without-brand/invite_member_mail_template_zh-CN.html new file mode 100644 index 0000000000..d4f80c66f8 --- /dev/null +++ b/api/templates/without-brand/invite_member_mail_template_zh-CN.html @@ -0,0 +1,69 @@ + + + + + + + +
+
+

尊敬的 {{ to }},

+

{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。

+

点击下方按钮即可登录 {{application_title}} 并且加入空间。

+

在此登录

+
+ +
+ + diff --git a/api/templates/without-brand/reset_password_mail_template_en-US.html b/api/templates/without-brand/reset_password_mail_template_en-US.html new file mode 100644 index 0000000000..a285ec74a9 --- /dev/null +++ b/api/templates/without-brand/reset_password_mail_template_en-US.html @@ -0,0 +1,70 @@ + + + + + + +
+

Set your {{application_title}} password

+

Copy and paste this code, this code will only be valid for the next 5 minutes.

+
+ {{code}} +
+

If you didn't request, don't worry. You can safely ignore this email.

+
+ + diff --git a/api/templates/without-brand/reset_password_mail_template_zh-CN.html b/api/templates/without-brand/reset_password_mail_template_zh-CN.html new file mode 100644 index 0000000000..3fbaf2e892 --- /dev/null +++ b/api/templates/without-brand/reset_password_mail_template_zh-CN.html @@ -0,0 +1,70 @@ + + + + + + +
+

设置您的 {{application_title}} 账户密码

+

复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。

+
+ {{code}} +
+

如果您没有请求,请不要担心。您可以安全地忽略此电子邮件。

+
+ + diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 1434a81b55..7d5d4cb52d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -15,17 +15,17 @@ import { } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' -import { useContextSelector } from 'use-context-selector' import s from './style.module.css' import cn from '@/utils/classnames' import { useStore } from '@/app/components/app/store' import AppSideBar from '@/app/components/app-sidebar' import type { NavIcon } from '@/app/components/app-sidebar/navLink' -import { fetchAppDetail, fetchAppSSO } from '@/service/apps' -import AppContext, { useAppContext } from '@/context/app-context' +import { fetchAppDetail } from '@/service/apps' +import { useAppContext } from '@/context/app-context' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import type { App } from '@/types/app' +import useDocumentTitle from '@/hooks/use-document-title' export type IAppDetailLayoutProps = { children: React.ReactNode @@ -56,7 +56,6 @@ const AppDetailLayout: FC = (props) => { icon: NavIcon selectedIcon: NavIcon }>>([]) - const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { const navs = [ @@ -96,9 +95,10 @@ const AppDetailLayout: FC = (props) => { return navs }, []) + useDocumentTitle(appDetail?.name || t('common.menus.appDetail')) + useEffect(() => { if (appDetail) { - document.title = `${(appDetail.name || 'App')} - Dify` const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand' const mode = isMobile ? 'collapse' : 'expand' setAppSiderbarExpand(isMobile ? mode : localeMode) @@ -142,14 +142,9 @@ const AppDetailLayout: FC = (props) => { else { setAppDetail({ ...res, enable_sso: false }) setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode)) - if (systemFeatures.enable_web_sso_switch_component && canIEditApp) { - fetchAppSSO({ appId }).then((ssoRes) => { - setAppDetail({ ...res, enable_sso: ssoRes.enabled }) - }) - } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, systemFeatures.enable_web_sso_switch_component]) + }, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace]) useUnmount(() => { setAppDetail() diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx index 79b45941f1..084adceef2 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx @@ -2,25 +2,22 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext, useContextSelector } from 'use-context-selector' +import { useContext } from 'use-context-selector' import AppCard from '@/app/components/app/overview/appCard' import Loading from '@/app/components/base/loading' import { ToastContext } from '@/app/components/base/toast' import { fetchAppDetail, - fetchAppSSO, - updateAppSSO, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus, } from '@/service/apps' -import type { App, AppSSO } from '@/types/app' +import type { App } from '@/types/app' import type { UpdateAppSiteCodeResponse } from '@/models/app' import { asyncRunSafe } from '@/utils' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import type { IAppCardProps } from '@/app/components/app/overview/appCard' import { useStore as useAppStore } from '@/app/components/app/store' -import AppContext from '@/context/app-context' export type ICardViewProps = { appId: string @@ -33,18 +30,11 @@ const CardView: FC = ({ appId, isInPanel, className }) => { const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) - const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) const updateAppDetail = async () => { try { const res = await fetchAppDetail({ url: '/apps', id: appId }) - if (systemFeatures.enable_web_sso_switch_component) { - const ssoRes = await fetchAppSSO({ appId }) - setAppDetail({ ...res, enable_sso: ssoRes.enabled }) - } - else { - setAppDetail({ ...res }) - } + setAppDetail({ ...res }) } catch (error) { console.error(error) } } @@ -95,16 +85,6 @@ const CardView: FC = ({ appId, isInPanel, className }) => { if (!err) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') - if (systemFeatures.enable_web_sso_switch_component) { - const [sso_err] = await asyncRunSafe( - updateAppSSO({ id: appId, enabled: Boolean(params.enable_sso) }) as Promise, - ) - if (sso_err) { - handleCallbackResult(sso_err) - return - } - } - handleCallbackResult(err) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx index dda198fc89..126bf45842 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx @@ -2,7 +2,9 @@ import type { FC } from 'react' import React, { useEffect } from 'react' import { useRouter } from 'next/navigation' +import { useTranslation } from 'react-i18next' import { useAppContext } from '@/context/app-context' +import useDocumentTitle from '@/hooks/use-document-title' export type IAppDetail = { children: React.ReactNode @@ -11,12 +13,13 @@ export type IAppDetail = { const AppDetail: FC = ({ children }) => { const router = useRouter() const { isCurrentWorkspaceDatasetOperator } = useAppContext() + const { t } = useTranslation() + useDocumentTitle(t('common.menus.appDetail')) useEffect(() => { if (isCurrentWorkspaceDatasetOperator) return router.replace('/datasets') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isCurrentWorkspaceDatasetOperator]) + }, [isCurrentWorkspaceDatasetOperator, router]) return ( <> diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 3f8c180c1a..468c8244c0 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -4,7 +4,8 @@ import { useContext, useContextSelector } from 'use-context-selector' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { RiMoreFill } from '@remixicon/react' +import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react' +import cn from '@/utils/classnames' import type { App } from '@/types/app' import Confirm from '@/app/components/base/confirm' import Toast, { ToastContext } from '@/app/components/base/toast' @@ -30,7 +31,10 @@ import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm- import { fetchWorkflowDraft } from '@/service/workflow' import { fetchInstalledAppList } from '@/service/explore' import { AppTypeIcon } from '@/app/components/app/type-selector' -import cn from '@/utils/classnames' +import Tooltip from '@/app/components/base/tooltip' +import AccessControl from '@/app/components/app/app-access-control' +import { AccessMode } from '@/models/access-control' +import { useGlobalPublicStore } from '@/context/global-public-context' export type AppCardProps = { app: App @@ -40,6 +44,7 @@ export type AppCardProps = { const AppCard = ({ app, onRefresh }: AppCardProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { isCurrentWorkspaceEditor } = useAppContext() const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() @@ -53,6 +58,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showSwitchModal, setShowSwitchModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [showAccessControl, setShowAccessControl] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const onConfirmDelete = useCallback(async () => { @@ -71,8 +77,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { }) } setShowConfirmDelete(false) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [app.id]) + }, [app.id, mutateApps, notify, onPlanInfoChanged, onRefresh, t]) const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, @@ -176,6 +181,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { setShowSwitchModal(false) } + const onUpdateAccessControl = useCallback(() => { + if (onRefresh) + onRefresh() + mutateApps() + setShowAccessControl(false) + }, [onRefresh, mutateApps, setShowAccessControl]) + const Operations = (props: HtmlContentProps) => { const onMouseLeave = async () => { props.onClose?.() @@ -198,18 +210,24 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { e.preventDefault() exportCheck() } - const onClickSwitch = async (e: React.MouseEvent) => { + const onClickSwitch = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() setShowSwitchModal(true) } - const onClickDelete = async (e: React.MouseEvent) => { + const onClickDelete = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() setShowConfirmDelete(true) } + const onClickAccessControl = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + setShowAccessControl(true) + } const onClickInstalledApp = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() @@ -226,41 +244,49 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { } } return ( -
- - - - {(app.mode === 'completion' || app.mode === 'chat') && ( <> - -
+
+ )} - - - -
+ { + systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && <> + + + + } +
+
) } @@ -302,6 +328,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { {app.mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
} +
+ {app.access_mode === AccessMode.PUBLIC && + + } + {app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && + + } + {app.access_mode === AccessMode.ORGANIZATION && + + } +
{ popupClassName={ (app.mode === 'completion' || app.mode === 'chat') ? '!w-[256px] translate-x-[-224px]' - : '!w-[160px] translate-x-[-128px]' + : '!w-[216px] translate-x-[-128px]' } className={'!z-20 h-fit'} /> @@ -419,6 +456,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { onClose={() => setSecretEnvList([])} /> )} + {showAccessControl && ( + setShowAccessControl(false)} /> + )} ) } diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index 1375f4dfd6..1b7ff39383 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -96,7 +96,6 @@ const Apps = () => { ] useEffect(() => { - document.title = `${t('common.menus.apps')} - Dify` if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) mutate() diff --git a/web/app/(commonLayout)/apps/layout.tsx b/web/app/(commonLayout)/apps/layout.tsx new file mode 100644 index 0000000000..10d04a4188 --- /dev/null +++ b/web/app/(commonLayout)/apps/layout.tsx @@ -0,0 +1,12 @@ +'use client' + +import useDocumentTitle from '@/hooks/use-document-title' +import { useTranslation } from 'react-i18next' + +export default function DatasetsLayout({ children }: { children: React.ReactNode }) { + const { t } = useTranslation() + useDocumentTitle(t('common.menus.apps')) + return (<> + {children} + ) +} diff --git a/web/app/(commonLayout)/apps/page.tsx b/web/app/(commonLayout)/apps/page.tsx index 4a146d9b65..3f617d41c9 100644 --- a/web/app/(commonLayout)/apps/page.tsx +++ b/web/app/(commonLayout)/apps/page.tsx @@ -1,24 +1,20 @@ 'use client' -import { useContextSelector } from 'use-context-selector' import { useTranslation } from 'react-i18next' import { RiDiscordFill, RiGithubFill } from '@remixicon/react' import Link from 'next/link' import style from '../list.module.css' import Apps from './Apps' -import AppContext from '@/context/app-context' -import { LicenseStatus } from '@/types/feature' import { useEducationInit } from '@/app/education-apply/hooks' +import { useGlobalPublicStore } from '@/context/global-public-context' const AppList = () => { const { t } = useTranslation() useEducationInit() - - const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) - + const { systemFeatures } = useGlobalPublicStore() return (
- {systemFeatures.license.status === LicenseStatus.NONE &&
+ {!systemFeatures.branding.enabled &&

{t('app.join')}

{t('app.communityIntro')}

diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 5619b1e445..94cd5ad562 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -31,6 +31,7 @@ import { getLocaleOnClient } from '@/i18n' import { useAppContext } from '@/context/app-context' import Tooltip from '@/app/components/base/tooltip' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' +import useDocumentTitle from '@/hooks/use-document-title' export type IAppDetailLayoutProps = { children: React.ReactNode @@ -158,10 +159,7 @@ const DatasetDetailLayout: FC = (props) => { return baseNavigation }, [datasetRes?.provider, datasetId, t]) - useEffect(() => { - if (datasetRes) - document.title = `${datasetRes.name || 'Dataset'} - Dify` - }, [datasetRes]) + useDocumentTitle(datasetRes?.name || t('common.menus.datasets')) const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand) diff --git a/web/app/(commonLayout)/datasets/Container.tsx b/web/app/(commonLayout)/datasets/Container.tsx index b484c0445d..62569ab26b 100644 --- a/web/app/(commonLayout)/datasets/Container.tsx +++ b/web/app/(commonLayout)/datasets/Container.tsx @@ -29,16 +29,18 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { useStore as useTagStore } from '@/app/components/base/tag-management/store' import { useAppContext } from '@/context/app-context' import { useExternalApiPanel } from '@/context/external-api-panel-context' +import { useGlobalPublicStore } from '@/context/global-public-context' +import useDocumentTitle from '@/hooks/use-document-title' const Container = () => { const { t } = useTranslation() + const { systemFeatures } = useGlobalPublicStore() const router = useRouter() const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext() const showTagManagementModal = useTagStore(s => s.showTagManagementModal) const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel() const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false) - - document.title = `${t('dataset.knowledge')} - Dify` + useDocumentTitle(t('dataset.knowledge')) const options = useMemo(() => { return [ @@ -125,7 +127,7 @@ const Container = () => { {activeTab === 'dataset' && ( <> - + {!systemFeatures.branding.enabled && } {showTagManagementModal && ( )} diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx index 6383513e9e..28461e8617 100644 --- a/web/app/(commonLayout)/datasets/Datasets.tsx +++ b/web/app/(commonLayout)/datasets/Datasets.tsx @@ -3,12 +3,12 @@ import { useCallback, useEffect, useRef } from 'react' import useSWRInfinite from 'swr/infinite' import { debounce } from 'lodash-es' -import { useTranslation } from 'react-i18next' import NewDatasetCard from './NewDatasetCard' import DatasetCard from './DatasetCard' import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets' import { fetchDatasets } from '@/service/datasets' import { useAppContext } from '@/context/app-context' +import { useTranslation } from 'react-i18next' const getKey = ( pageIndex: number, @@ -48,6 +48,7 @@ const Datasets = ({ keywords, includeAll, }: Props) => { + const { t } = useTranslation() const { isCurrentWorkspaceEditor } = useAppContext() const { data, isLoading, setSize, mutate } = useSWRInfinite( (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll), @@ -57,11 +58,8 @@ const Datasets = ({ const loadingStateRef = useRef(false) const anchorRef = useRef(null) - const { t } = useTranslation() - useEffect(() => { loadingStateRef.current = isLoading - document.title = `${t('dataset.knowledge')} - Dify` }, [isLoading, t]) const onScroll = useCallback( @@ -87,7 +85,7 @@ const Datasets = ({ return (
- - )} {/* more settings switch */} {!isShowMore && ( @@ -392,14 +367,14 @@ const SettingsModal: FC = ({ )}
{t(`${prefixSettings}.more.copyrightTooltip`)}
+
{t(`${prefixSettings}.more.copyrightTooltip`)}
} asChild={false} > setInputInfo({ ...inputInfo, copyrightSwitchValue: v })} /> @@ -450,7 +425,6 @@ const SettingsModal: FC = ({
- {showAppIconPicker && (
e.stopPropagation()}> ({ + accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + userCanAccess: false, currentConversationId: '', appPrevChatTree: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 91ceaffd1e..d03a28ae57 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -43,6 +43,9 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { noop } from 'lodash-es' +import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -72,7 +75,18 @@ function getFormattedChatList(messages: any[]) { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) + const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ + appId: installedAppInfo?.app.id || appInfo?.app_id, + isInstalledApp, + enabled: systemFeatures.webapp_auth.enabled, + }) + const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ + appId: installedAppInfo?.app.id || appInfo?.app_id, + isInstalledApp, + enabled: systemFeatures.webapp_auth.enabled, + }) useAppFavicon({ enable: !installedAppInfo, @@ -447,7 +461,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return { appInfoError, - appInfoLoading, + appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), + accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, + userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, currentConversationId, diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index dfd7bd21a7..2ba50ea49b 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -20,6 +20,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { checkOrSetAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import cn from '@/utils/classnames' +import useDocumentTitle from '@/hooks/use-document-title' type ChatWithHistoryProps = { className?: string @@ -28,6 +29,7 @@ const ChatWithHistory: FC = ({ className, }) => { const { + userCanAccess, appInfoError, appData, appInfoLoading, @@ -45,19 +47,17 @@ const ChatWithHistory: FC = ({ useEffect(() => { themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted) - if (site) { - if (customConfig) - document.title = `${site.title}` - else - document.title = `${site.title} - Powered by Dify` - } }, [site, customConfig, themeBuilder]) + useDocumentTitle(site?.title || 'Chat') + if (appInfoLoading) { return ( ) } + if (!userCanAccess) + return if (appInfoError) { return ( @@ -124,6 +124,8 @@ const ChatWithHistoryWrap: FC = ({ const { appInfoError, appInfoLoading, + accessMode, + userCanAccess, appData, appParams, appMeta, @@ -166,6 +168,8 @@ const ChatWithHistoryWrap: FC = ({ appInfoError, appInfoLoading, appData, + accessMode, + userCanAccess, appParams, appMeta, appChatListDataLoading, diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index dc4e864728..5fde657f68 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -19,6 +19,8 @@ import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/re import DifyLogo from '@/app/components/base/logo/dify-logo' import type { ConversationItem } from '@/models/share' import cn from '@/utils/classnames' +import { AccessMode } from '@/models/access-control' +import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { isPanel?: boolean @@ -27,6 +29,8 @@ type Props = { const Sidebar = ({ isPanel }: Props) => { const { t } = useTranslation() const { + isInstalledApp, + accessMode, appData, handleNewConversation, pinnedConversationList, @@ -44,7 +48,7 @@ const Sidebar = ({ isPanel }: Props) => { isResponding, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState - + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const [showConfirm, setShowConfirm] = useState(null) const [showRename, setShowRename] = useState(null) @@ -136,7 +140,7 @@ const Sidebar = ({ isPanel }: Props) => { )}
- + {/* powered by */}
{!appData?.custom_config?.remove_webapp_brand && ( @@ -144,34 +148,33 @@ const Sidebar = ({ isPanel }: Props) => { 'flex shrink-0 items-center gap-1.5 px-1', )}>
{t('share.chat.poweredBy')}
- {appData?.custom_config?.replace_webapp_logo && ( - logo - )} - {!appData?.custom_config?.replace_webapp_logo && ( - - )} + {systemFeatures.branding.enabled ? ( + logo + ) : ( + ) + }
)}
+ {!!showConfirm && ( + + )} + {showRename && ( + + )} - {!!showConfirm && ( - - )} - {showRename && ( - - )} ) } diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index fb00dbd64d..0dd6e7a29a 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -15,8 +15,11 @@ import type { ConversationItem, } from '@/models/share' import { noop } from 'lodash-es' +import { AccessMode } from '@/models/access-control' export type EmbeddedChatbotContextValue = { + accessMode?: AccessMode + userCanAccess?: boolean appInfoError?: any appInfoLoading?: boolean appMeta?: AppMeta @@ -53,6 +56,8 @@ export type EmbeddedChatbotContextValue = { } export const EmbeddedChatbotContext = createContext({ + userCanAccess: false, + accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, currentConversationId: '', appPrevChatList: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 7efbc95dee..ce42575bc9 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -36,6 +36,9 @@ import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { noop } from 'lodash-es' +import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -65,7 +68,18 @@ function getFormattedChatList(messages: any[]) { export const useEmbeddedChatbot = () => { const isInstalledApp = false + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) + const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ + appId: appInfo?.app_id, + isInstalledApp, + enabled: systemFeatures.webapp_auth.enabled, + }) + const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ + appId: appInfo?.app_id, + isInstalledApp, + enabled: systemFeatures.webapp_auth.enabled, + }) const appData = useMemo(() => { return appInfo @@ -364,7 +378,9 @@ export const useEmbeddedChatbot = () => { return { appInfoError, - appInfoLoading, + appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), + accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, + userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, allowResetChat, appId, diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx index 59c358a7a6..49189c419e 100644 --- a/web/app/components/base/chat/embedded-chatbot/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/index.tsx @@ -21,9 +21,11 @@ import Header from '@/app/components/base/chat/embedded-chatbot/header' import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper' import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' +import useDocumentTitle from '@/hooks/use-document-title' const Chatbot = () => { const { + userCanAccess, isMobile, allowResetChat, appInfoError, @@ -43,14 +45,10 @@ const Chatbot = () => { useEffect(() => { themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted) - if (site) { - if (customConfig) - document.title = `${site.title}` - else - document.title = `${site.title} - Powered by Dify` - } }, [site, customConfig, themeBuilder]) + useDocumentTitle(site?.title || 'Chat') + if (appInfoLoading) { return ( <> @@ -66,6 +64,9 @@ const Chatbot = () => { ) } + if (!userCanAccess) + return + if (appInfoError) { return ( <> @@ -137,6 +138,8 @@ const EmbeddedChatbotWrapper = () => { appInfoError, appInfoLoading, appData, + accessMode, + userCanAccess, appParams, appMeta, appChatListDataLoading, @@ -168,6 +171,8 @@ const EmbeddedChatbotWrapper = () => { } = useEmbeddedChatbot() return = { @@ -32,10 +32,15 @@ const DifyLogo: FC = ({ }) => { const { theme } = useTheme() const themedStyle = (theme === 'dark' && style === 'default') ? 'monochromeWhite' : style + const { systemFeatures } = useGlobalPublicStore() + + let src = `${basePath}${logoPathMap[themedStyle]}` + if (systemFeatures.branding.enabled) + src = systemFeatures.branding.workspace_logo return ( Dify logo diff --git a/web/app/components/base/svg-gallery/index.tsx b/web/app/components/base/svg-gallery/index.tsx index 94fc82c740..710a0107fb 100644 --- a/web/app/components/base/svg-gallery/index.tsx +++ b/web/app/components/base/svg-gallery/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import { SVG } from '@svgdotjs/svg.js' -import ImagePreview from '@/app/components/base/image-uploader/image-preview' import DOMPurify from 'dompurify' +import ImagePreview from '@/app/components/base/image-uploader/image-preview' export const SVGRenderer = ({ content }: { content: string }) => { const svgRef = useRef(null) diff --git a/web/app/components/base/tooltip/index.tsx b/web/app/components/base/tooltip/index.tsx index e6c4de31f1..53f36be5fb 100644 --- a/web/app/components/base/tooltip/index.tsx +++ b/web/app/components/base/tooltip/index.tsx @@ -92,6 +92,7 @@ const Tooltip: FC = ({ }} onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)} asChild={asChild} + className={!asChild ? triggerClassName : ''} > {children ||
} diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 2f5728ceef..506ef46799 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -94,6 +94,11 @@ export type CurrentPlanInfoBackend = { education: { enabled: boolean activated: boolean + }, + webapp_copyright_enabled: boolean + workspace_members: { + size: number + limit: number } } diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index 38c885ebe2..11664c7ada 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -21,6 +21,7 @@ import VectorSpaceFull from '@/app/components/billing/vector-space-full' import classNames from '@/utils/classnames' import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others' import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config' + type IStepOneProps = { datasetId?: string dataSourceType?: DataSourceType @@ -45,7 +46,8 @@ type IStepOneProps = { type NotionConnectorProps = { onSetting: () => void } -export const NotionConnector = ({ onSetting }: NotionConnectorProps) => { +export const NotionConnector = (props: NotionConnectorProps) => { + const { onSetting } = props const { t } = useTranslation() return ( @@ -162,7 +164,7 @@ const StepOne = ({ > {t('datasetCreation.stepOne.dataSourceType.file')} @@ -185,7 +187,7 @@ const StepOne = ({ > {t('datasetCreation.stepOne.dataSourceType.notion')} @@ -193,21 +195,21 @@ const StepOne = ({ {(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
changeType(DataSourceType.WEB)} + className={cn( + s.dataSourceItem, + 'system-sm-medium', + dataSourceType === DataSourceType.WEB && s.active, + dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled, + )} + onClick={() => changeType(DataSourceType.WEB)} > - - - {t('datasetCreation.stepOne.dataSourceType.web')} - + + + {t('datasetCreation.stepOne.dataSourceType.web')} +
)} diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index 8fa0776ee3..69d955b11f 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -15,7 +15,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ### 鉴权 - Dify Service API 使用 `API-Key` 进行鉴权。 + Service API 使用 `API-Key` 进行鉴权。 **强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。** 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index 0cc3dab79e..17690ec3d0 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -14,7 +14,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 ### Authentication - Dify Service API 使用 `API-Key` 进行鉴权。 + Service API 使用 `API-Key` 进行鉴权。 **强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。** 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx index 5175b46377..bae2610cba 100644 --- a/web/app/components/explore/index.tsx +++ b/web/app/components/explore/index.tsx @@ -2,12 +2,13 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' -import { useTranslation } from 'react-i18next' import ExploreContext from '@/context/explore-context' import Sidebar from '@/app/components/explore/sidebar' import { useAppContext } from '@/context/app-context' import { fetchMembers } from '@/service/common' import type { InstalledApp } from '@/models/explore' +import { useTranslation } from 'react-i18next' +import useDocumentTitle from '@/hooks/use-document-title' export type IExploreProps = { children: React.ReactNode @@ -16,15 +17,16 @@ export type IExploreProps = { const Explore: FC = ({ children, }) => { - const { t } = useTranslation() const router = useRouter() const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0) const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() const [hasEditPermission, setHasEditPermission] = useState(false) const [installedApps, setInstalledApps] = useState([]) + const { t } = useTranslation() + + useDocumentTitle(t('common.menus.explore')) useEffect(() => { - document.title = `${t('explore.title')} - Dify`; (async () => { const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} }) if (!accounts) diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 62f9452f88..71013fc2e1 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -26,15 +26,15 @@ const InstalledApp: FC = ({ } return ( -
+
{installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && ( )} {installedApp.app.mode === 'completion' && ( - + )} {installedApp.app.mode === 'workflow' && ( - + )}
) diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 4a08a4c03d..6648a4a2e8 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next' import { Fragment, useState } from 'react' import { useRouter } from 'next/navigation' -import { useContextSelector } from 'use-context-selector' import { RiAccountCircleLine, RiArrowRightUpLine, @@ -28,12 +27,12 @@ import { useGetDocLanguage } from '@/context/i18n' import Avatar from '@/app/components/base/avatar' import ThemeSwitcher from '@/app/components/base/theme-switcher' import { logout } from '@/service/common' -import AppContext, { useAppContext } from '@/context/app-context' +import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' -import { LicenseStatus } from '@/types/feature' import { IS_CLOUD_EDITION } from '@/config' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' export default function AppSelector() { const itemClassName = ` @@ -42,7 +41,7 @@ export default function AppSelector() { ` const router = useRouter() const [aboutVisible, setAboutVisible] = useState(false) - const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) + const { systemFeatures } = useGlobalPublicStore() const { t } = useTranslation() const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext() @@ -127,73 +126,75 @@ export default function AppSelector() {
-
- - - -
{t('common.userProfile.helpCenter')}
- - -
- - {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && } -
-
- - - -
{t('common.userProfile.roadmap')}
- - -
- {systemFeatures.license.status === LicenseStatus.NONE && - - -
{t('common.userProfile.github')}
-
- - -
- -
} - { - document?.body?.getAttribute('data-public-site-about') !== 'hide' && ( - -
+
+ + setAboutVisible(true)}> - -
{t('common.userProfile.about')}
-
-
{langeniusVersionInfo.current_version}
- -
+ )} + href={`https://docs.dify.ai/${docLanguage}/introduction`} + target='_blank' rel='noopener noreferrer'> + +
{t('common.userProfile.helpCenter')}
+ + +
+ + {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && } +
+
+ + + +
{t('common.userProfile.roadmap')}
+ + +
+ + + +
{t('common.userProfile.github')}
+
+ +
-
- ) - } -
+ + + { + document?.body?.getAttribute('data-public-site-about') !== 'hide' && ( + +
setAboutVisible(true)}> + +
{t('common.userProfile.about')}
+
+
{langeniusVersionInfo.current_version}
+ +
+
+
+ ) + } +
+ }
{t('common.theme.theme')}
- +
diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index 939834edeb..df7aae6b62 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -25,6 +25,7 @@ import { LanguagesSupported } from '@/i18n/language' import cn from '@/utils/classnames' import Tooltip from '@/app/components/base/tooltip' import { RiPencilLine } from '@remixicon/react' +import { useGlobalPublicStore } from '@/context/global-public-context' dayjs.extend(relativeTime) const MembersPage = () => { @@ -38,7 +39,7 @@ const MembersPage = () => { } const { locale } = useContext(I18n) - const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext() + const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext() const { data, mutate } = useSWR( { url: '/workspaces/current/members', @@ -46,6 +47,7 @@ const MembersPage = () => { }, fetchMembers, ) + const { systemFeatures } = useGlobalPublicStore() const [inviteModalVisible, setInviteModalVisible] = useState(false) const [invitationResults, setInvitationResults] = useState([]) const [invitedModalVisible, setInvitedModalVisible] = useState(false) diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 107166bc31..e999253ea7 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useContext } from 'use-context-selector' import { RiCloseLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' @@ -18,6 +18,7 @@ import I18n from '@/context/i18n' import 'react-multi-email/dist/style.css' import { noop } from 'lodash-es' +import { useProviderContextSelector } from '@/context/provider-context' type IInviteModalProps = { isEmailSetup: boolean onCancel: () => void @@ -30,13 +31,27 @@ const InviteModal = ({ onSend, }: IInviteModalProps) => { const { t } = useTranslation() + const licenseLimit = useProviderContextSelector(s => s.licenseLimit) + const refreshLicenseLimit = useProviderContextSelector(s => s.refreshLicenseLimit) const [emails, setEmails] = useState([]) const { notify } = useContext(ToastContext) + const [isLimited, setIsLimited] = useState(false) + const [isLimitExceeded, setIsLimitExceeded] = useState(false) + const [usedSize, setUsedSize] = useState(licenseLimit.workspace_members.size ?? 0) + useEffect(() => { + const limited = licenseLimit.workspace_members.limit > 0 + const used = emails.length + licenseLimit.workspace_members.size + setIsLimited(limited) + setUsedSize(used) + setIsLimitExceeded(limited && (used > licenseLimit.workspace_members.limit)) + }, [licenseLimit, emails]) const { locale } = useContext(I18n) const [role, setRole] = useState('normal') const handleSend = useCallback(async () => { + if (isLimitExceeded) + return if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) { try { const { result, invitation_results } = await inviteMember({ @@ -45,6 +60,7 @@ const InviteModal = ({ }) if (result === 'success') { + refreshLicenseLimit() onCancel() onSend(invitation_results) } @@ -54,7 +70,7 @@ const InviteModal = ({ else { notify({ type: 'error', message: t('common.members.emailInvalid') }) } - }, [role, emails, notify, onCancel, onSend, t]) + }, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t]) return (
@@ -82,7 +98,7 @@ const InviteModal = ({
{t('common.members.email')}
-
+
+
licenseLimit.workspace_members.limit) ? 'text-text-destructive' : '')} + > + {usedSize} + / + {isLimited ? licenseLimit.workspace_members.limit : t('common.license.unlimited')} +
@@ -109,7 +133,7 @@ const InviteModal = ({ tabIndex={0} className='w-full' onClick={handleSend} - disabled={!emails.length} + disabled={!emails.length || isLimitExceeded} variant='primary' > {t('common.members.sendInvite')} diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 7c4e2eac3c..4aa98daf66 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -23,7 +23,7 @@ import { import InstallFromMarketplace from './install-from-marketplace' import { useProviderContext } from '@/context/provider-context' import cn from '@/utils/classnames' -import { useSelector as useAppContextSelector } from '@/context/app-context' +import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { searchText: string @@ -40,7 +40,7 @@ const ModelProviderPage = ({ searchText }: Props) => { const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts) const { modelProviders: providers } = useProviderContext() - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel const [configuredProviders, notConfiguredProviders] = useMemo(() => { const configuredProviders: ModelProvider[] = [] diff --git a/web/app/components/header/license-env/index.tsx b/web/app/components/header/license-env/index.tsx index 86c53d7c83..8946143415 100644 --- a/web/app/components/header/license-env/index.tsx +++ b/web/app/components/header/license-env/index.tsx @@ -1,16 +1,15 @@ 'use client' -import AppContext from '@/context/app-context' import { LicenseStatus } from '@/types/feature' import { useTranslation } from 'react-i18next' -import { useContextSelector } from 'use-context-selector' import dayjs from 'dayjs' import PremiumBadge from '../../base/premium-badge' import { RiHourglass2Fill } from '@remixicon/react' +import { useGlobalPublicStore } from '@/context/global-public-context' const LicenseNav = () => { const { t } = useTranslation() - const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures) + const { systemFeatures } = useGlobalPublicStore() if (systemFeatures.license?.status === LicenseStatus.EXPIRING) { const expiredAt = systemFeatures.license?.expired_at diff --git a/web/app/components/plugins/plugin-page/context.tsx b/web/app/components/plugins/plugin-page/context.tsx index ae1ad7d053..52efbb263e 100644 --- a/web/app/components/plugins/plugin-page/context.tsx +++ b/web/app/components/plugins/plugin-page/context.tsx @@ -10,11 +10,11 @@ import { createContext, useContextSelector, } from 'use-context-selector' -import { useSelector as useAppContextSelector } from '@/context/app-context' import type { FilterState } from './filter-management' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { noop } from 'lodash-es' import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks' +import { useGlobalPublicStore } from '@/context/global-public-context' export type PluginPageContextValue = { containerRef: React.RefObject @@ -61,7 +61,7 @@ export const PluginPageContextProvider = ({ }) const [currentPluginID, setCurrentPluginID] = useState() - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const tabs = usePluginPageTabs() const options = useMemo(() => { return enable_marketplace ? tabs : tabs.filter(tab => tab.value !== PLUGIN_PAGE_TABS_MAP.marketplace) diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx index 53a00dc01c..139567a1b5 100644 --- a/web/app/components/plugins/plugin-page/empty/index.tsx +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -6,19 +6,19 @@ import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-f import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package' import { usePluginPageContext } from '../context' import { Group } from '@/app/components/base/icons/src/vender/other' -import { useSelector as useAppContextSelector } from '@/context/app-context' import Line from '../../marketplace/empty/line' import { useInstalledPluginList } from '@/service/use-plugins' import { useTranslation } from 'react-i18next' import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import { noop } from 'lodash-es' +import { useGlobalPublicStore } from '@/context/global-public-context' const Empty = () => { const { t } = useTranslation() const fileInputRef = useRef(null) const [selectedAction, setSelectedAction] = useState(null) const [selectedFile, setSelectedFile] = useState(null) - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const setActiveTab = usePluginPageContext(v => v.setActiveTab) const handleFileChange = (event: React.ChangeEvent) => { diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index cb57b7f4c6..7cba865c66 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -25,7 +25,6 @@ import TabSlider from '@/app/components/base/tab-slider' import Tooltip from '@/app/components/base/tooltip' import cn from '@/utils/classnames' import PermissionSetModal from '@/app/components/plugins/permission-setting-modal/modal' -import { useSelector as useAppContextSelector } from '@/context/app-context' import InstallFromMarketplace from '../install-plugin/install-from-marketplace' import { useRouter, @@ -42,6 +41,8 @@ import I18n from '@/context/i18n' import { noop } from 'lodash-es' import { PLUGIN_TYPE_SEARCH_MAP } from '../marketplace/plugin-type-switch' import { PLUGIN_PAGE_TABS_MAP } from '../hooks' +import { useGlobalPublicStore } from '@/context/global-public-context' +import useDocumentTitle from '@/hooks/use-document-title' const PACKAGE_IDS_KEY = 'package-ids' const BUNDLE_INFO_KEY = 'bundle-info' @@ -58,8 +59,7 @@ const PluginPage = ({ const { locale } = useContext(I18n) const searchParams = useSearchParams() const { replace } = useRouter() - - document.title = `${t('plugin.metadata.title')} - Dify` + useDocumentTitle(t('plugin.metadata.title')) // just support install one package now const packageId = useMemo(() => { @@ -136,7 +136,7 @@ const PluginPage = ({ const options = usePluginPageContext(v => v.options) const activeTab = usePluginPageContext(v => v.activeTab) const setActiveTab = usePluginPageContext(v => v.setActiveTab) - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab]) const isExploringMarketplace = useMemo(() => { diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 875cbd0ab2..abe4f9cb6a 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -14,10 +14,10 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import { useSelector as useAppContextSelector } from '@/context/app-context' import { useTranslation } from 'react-i18next' import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import { noop } from 'lodash-es' +import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { onSwitchToMarketplaceTab: () => void @@ -30,7 +30,7 @@ const InstallPluginDropdown = ({ const [isMenuOpen, setIsMenuOpen] = useState(false) const [selectedAction, setSelectedAction] = useState(null) const [selectedFile, setSelectedFile] = useState(null) - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0] diff --git a/web/app/components/plugins/plugin-page/use-permission.ts b/web/app/components/plugins/plugin-page/use-permission.ts index 93c96a8926..918813fb44 100644 --- a/web/app/components/plugins/plugin-page/use-permission.ts +++ b/web/app/components/plugins/plugin-page/use-permission.ts @@ -3,8 +3,8 @@ import { useAppContext } from '@/context/app-context' import Toast from '../../base/toast' import { useTranslation } from 'react-i18next' import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins' -import { useSelector as useAppContextSelector } from '@/context/app-context' import { useMemo } from 'react' +import { useGlobalPublicStore } from '@/context/global-public-context' const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => { if (!permission) @@ -46,7 +46,7 @@ const usePermission = () => { } export const useCanInstallPluginFromMarketplace = () => { - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { canManagement } = usePermission() const canInstallPluginFromMarketplace = useMemo(() => { diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 5751092494..5450fa7ce6 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -13,6 +13,7 @@ import { checkOrSetAccessToken } from '../utils' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' +import AppUnavailable from '../../base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import RunOnce from '@/app/components/share/text-generation/run-once' import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' @@ -38,6 +39,10 @@ import { Resolution, TransferMethod } from '@/types/app' import { useAppFavicon } from '@/hooks/use-app-favicon' import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' +import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' +import { AccessMode } from '@/models/access-control' +import { useGlobalPublicStore } from '@/context/global-public-context' +import useDocumentTitle from '@/hooks/use-document-title' const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. enum TaskStatus { @@ -98,14 +103,25 @@ const TextGeneration: FC = ({ doSetInputs(newInputs) inputsRef.current = newInputs }, []) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const [appId, setAppId] = useState('') const [siteInfo, setSiteInfo] = useState(null) - const [canReplaceLogo, setCanReplaceLogo] = useState(false) const [customConfig, setCustomConfig] = useState | null>(null) const [promptConfig, setPromptConfig] = useState(null) const [moreLikeThisConfig, setMoreLikeThisConfig] = useState(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) + const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ + appId, + isInstalledApp, + enabled: systemFeatures.webapp_auth.enabled, + }) + const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ + appId, + isInstalledApp, + enabled: systemFeatures.webapp_auth.enabled, + }) + // save message const [savedMessages, setSavedMessages] = useState([]) const fetchSavedMessage = async () => { @@ -395,10 +411,9 @@ const TextGeneration: FC = ({ useEffect(() => { (async () => { const [appData, appParams]: any = await fetchInitData() - const { app_id: appId, site: siteInfo, can_replace_logo, custom_config } = appData + const { app_id: appId, site: siteInfo, custom_config } = appData setAppId(appId) setSiteInfo(siteInfo as SiteInfo) - setCanReplaceLogo(can_replace_logo) setCustomConfig(custom_config) changeLanguage(siteInfo.default_language) @@ -422,14 +437,7 @@ const TextGeneration: FC = ({ }, []) // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. - useEffect(() => { - if (siteInfo?.title) { - if (canReplaceLogo) - document.title = `${siteInfo.title}` - else - document.title = `${siteInfo.title} - Powered by Dify` - } - }, [siteInfo?.title, canReplaceLogo]) + useDocumentTitle(siteInfo?.title || t('share.generation.title')) useAppFavicon({ enable: !isInstalledApp, @@ -528,12 +536,14 @@ const TextGeneration: FC = ({
) - if (!appId || !siteInfo || !promptConfig) { + if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { return (
) } + if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) + return return (
= ({ imageUrl={siteInfo.icon_url} />
{siteInfo.title}
- +
{siteInfo.description && (
{siteInfo.description}
@@ -631,10 +641,9 @@ const TextGeneration: FC = ({ !isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular', )}>
{t('share.chat.poweredBy')}
- {customConfig?.replace_webapp_logo && ( - logo - )} - {!customConfig?.replace_webapp_logo && ( + {systemFeatures.branding.enabled ? ( + logo + ) : ( )}
diff --git a/web/app/components/share/text-generation/info-modal.tsx b/web/app/components/share/text-generation/info-modal.tsx index 9ed584be2f..156270fc85 100644 --- a/web/app/components/share/text-generation/info-modal.tsx +++ b/web/app/components/share/text-generation/info-modal.tsx @@ -1,9 +1,9 @@ import React from 'react' +import cn from 'classnames' import Modal from '@/app/components/base/modal' import AppIcon from '@/app/components/base/app-icon' import type { SiteInfo } from '@/models/share' import { appDefaultIconBackground } from '@/config' -import cn from 'classnames' type Props = { data?: SiteInfo diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index d038b94c1a..19b660b083 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -6,27 +6,32 @@ import type { Placement } from '@floating-ui/react' import { RiEqualizer2Line, } from '@remixicon/react' +import { useRouter } from 'next/navigation' +import Divider from '../../base/divider' +import { removeAccessToken } from '../utils' +import InfoModal from './info-modal' import ActionButton from '@/app/components/base/action-button' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import Divider from '@/app/components/base/divider' import ThemeSwitcher from '@/app/components/base/theme-switcher' -import InfoModal from './info-modal' import type { SiteInfo } from '@/models/share' import cn from '@/utils/classnames' type Props = { data?: SiteInfo placement?: Placement + hideLogout?: boolean } const MenuDropdown: FC = ({ data, placement, + hideLogout, }) => { + const router = useRouter() const { t } = useTranslation() const [open, doSetOpen] = useState(false) const openRef = useRef(open) @@ -39,6 +44,11 @@ const MenuDropdown: FC = ({ setOpen(!openRef.current) }, [setOpen]) + const handleLogout = useCallback(() => { + removeAccessToken() + router.replace(`/webapp-signin?redirect_url=${window.location.href}`) + }, [router]) + const [show, setShow] = useState(false) return ( @@ -64,7 +74,7 @@ const MenuDropdown: FC = ({
{t('common.theme.theme')}
- +
diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index b1da6d5611..0970daab9c 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -15,14 +15,14 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' -import { useSelector as useAppContextSelector } from '@/context/app-context' import { useAllToolProviders } from '@/service/use-tools' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' +import { useGlobalPublicStore } from '@/context/global-public-context' const ProviderList = () => { const { t } = useTranslation() + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const containerRef = useRef(null) - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) const [activeTab, setActiveTab] = useTabSearchParams({ defaultTab: 'builtin', @@ -144,8 +144,8 @@ const ProviderList = () => { /> ) } -
-
+
+ {currentProvider && !currentProvider.plugin_id && ( { const { t } = useTranslation() @@ -36,7 +35,6 @@ const FeaturesTrigger = () => { const appDetail = useAppStore(s => s.appDetail) const appID = appDetail?.id const setAppDetail = useAppStore(s => s.setAppDetail) - const systemFeatures = useAppSelector(state => state.systemFeatures) const { nodesReadOnly, getNodesReadOnly, @@ -85,18 +83,12 @@ const FeaturesTrigger = () => { const updateAppDetail = useCallback(async () => { try { const res = await fetchAppDetail({ url: '/apps', id: appID! }) - if (systemFeatures.enable_web_sso_switch_component) { - const ssoRes = await fetchAppSSO({ appId: appID! }) - setAppDetail({ ...res, enable_sso: ssoRes.enabled }) - } - else { - setAppDetail({ ...res }) - } + setAppDetail({ ...res }) } catch (error) { console.error(error) } - }, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component]) + }, [appID, setAppDetail]) const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) const onPublish = useCallback(async (params?: PublishWorkflowParams) => { if (await handleCheckBeforePublish()) { diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index 3ad0a41d54..36831aee3c 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -21,7 +21,7 @@ import ActionButton from '../../base/action-button' import { RiAddLine } from '@remixicon/react' import { PluginType } from '../../plugins/types' import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' -import { useSelector as useAppContextSelector } from '@/context/app-context' +import { useGlobalPublicStore } from '@/context/global-public-context' type AllToolsProps = { className?: string @@ -87,7 +87,7 @@ const AllTools = ({ plugins: notInstalledPlugins = [], } = useMarketplacePlugins() - const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) useEffect(() => { if (enable_marketplace) return diff --git a/web/app/components/workflow/run/iteration-log/iteration-result-panel.tsx b/web/app/components/workflow/run/iteration-log/iteration-result-panel.tsx index bba39bf4d6..3d9ad87890 100644 --- a/web/app/components/workflow/run/iteration-log/iteration-result-panel.tsx +++ b/web/app/components/workflow/run/iteration-log/iteration-result-panel.tsx @@ -112,7 +112,7 @@ const IterationResultPanel: FC = ({ 'transition-all duration-200', expandedIterations[index] ? 'opacity-100' - : 'max-h-0 opacity-0 overflow-hidden', + : 'max-h-0 overflow-hidden opacity-0', )}> = ({ 'transition-all duration-200', expandedLoops[index] ? 'opacity-100' - : 'max-h-0 opacity-0 overflow-hidden', + : 'max-h-0 overflow-hidden opacity-0', )}> { loopVariableMap?.[index] && ( diff --git a/web/app/components/workflow/run/loop-result-panel.tsx b/web/app/components/workflow/run/loop-result-panel.tsx index 7176a4a2b2..836bef8819 100644 --- a/web/app/components/workflow/run/loop-result-panel.tsx +++ b/web/app/components/workflow/run/loop-result-panel.tsx @@ -85,7 +85,7 @@ const LoopResultPanel: FC = ({ 'transition-all duration-200', expandedLoops[index] ? 'opacity-100' - : 'max-h-0 opacity-0 overflow-hidden', + : 'max-h-0 overflow-hidden opacity-0', )}> { + useDocumentTitle('') const searchParams = useSearchParams() const token = searchParams.get('token') + const { systemFeatures } = useGlobalPublicStore() return (
{token ? : } -
+ {!systemFeatures.branding.enabled &&
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. -
+
}
) diff --git a/web/app/init/InitPasswordPopup.tsx b/web/app/init/InitPasswordPopup.tsx index 464da9edea..6c0e9e3078 100644 --- a/web/app/init/InitPasswordPopup.tsx +++ b/web/app/init/InitPasswordPopup.tsx @@ -8,8 +8,10 @@ import Button from '@/app/components/base/button' import { basePath } from '@/utils/var' import { fetchInitValidateStatus, initValidate } from '@/service/common' import type { InitValidateStatusResponse } from '@/models/common' +import useDocumentTitle from '@/hooks/use-document-title' const InitPasswordPopup = () => { + useDocumentTitle('') const [password, setPassword] = useState('') const [loading, setLoading] = useState(true) const [validated, setValidated] = useState(false) diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index c01be722c0..e549eba521 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -16,6 +16,7 @@ import Button from '@/app/components/base/button' import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common' import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' +import useDocumentTitle from '@/hooks/use-document-title' const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ @@ -33,6 +34,7 @@ const accountFormSchema = z.object({ type AccountFormValues = z.infer const InstallForm = () => { + useDocumentTitle('') const { t } = useTranslation() const router = useRouter() const [showPassword, setShowPassword] = React.useState(false) diff --git a/web/app/install/page.tsx b/web/app/install/page.tsx index f63fdf8443..304f44b35c 100644 --- a/web/app/install/page.tsx +++ b/web/app/install/page.tsx @@ -1,17 +1,20 @@ +'use client' import React from 'react' import Header from '../signin/_header' import InstallForm from './installForm' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' const Install = () => { + const { systemFeatures } = useGlobalPublicStore() return (
-
+ {!systemFeatures.branding.enabled &&
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. -
+
}
) diff --git a/web/app/layout.tsx b/web/app/layout.tsx index dc4f540c0d..e39b4f3a1b 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,5 +1,5 @@ -import type { Viewport } from 'next' import RoutePrefixHandle from './routePrefixHandle' +import type { Viewport } from 'next' import I18nServer from './components/i18n-server' import BrowserInitor from './components/browser-initor' import SentryInitor from './components/sentry-initor' @@ -8,10 +8,7 @@ import { TanstackQueryIniter } from '@/context/query-client' import { ThemeProvider } from 'next-themes' import './styles/globals.css' import './styles/markdown.scss' - -export const metadata = { - title: 'Dify', -} +import GlobalPublicStoreProvider from '@/context/global-public-context' export const viewport: Viewport = { width: 'device-width', @@ -68,7 +65,9 @@ const LocaleLayout = async ({ disableTransitionOnChange > - {children} + + {children} + diff --git a/web/app/reset-password/layout.tsx b/web/app/reset-password/layout.tsx index 3d053e4d34..979c64e7c9 100644 --- a/web/app/reset-password/layout.tsx +++ b/web/app/reset-password/layout.tsx @@ -1,8 +1,11 @@ +'use client' import Header from '../signin/_header' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' -export default async function SignInLayout({ children }: any) { +export default function SignInLayout({ children }: any) { + const { systemFeatures } = useGlobalPublicStore() return <>
@@ -18,9 +21,9 @@ export default async function SignInLayout({ children }: any) { {children}
-
+ {!systemFeatures.branding.enabled &&
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. -
+
} diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx index dd8cdbc69e..8a9bf78eb9 100644 --- a/web/app/reset-password/page.tsx +++ b/web/app/reset-password/page.tsx @@ -13,9 +13,11 @@ import Toast from '@/app/components/base/toast' import { sendResetPasswordCode } from '@/service/common' import I18NContext from '@/context/i18n' import { noop } from 'lodash-es' +import useDocumentTitle from '@/hooks/use-document-title' export default function CheckCode() { const { t } = useTranslation() + useDocumentTitle('') const searchParams = useSearchParams() const router = useRouter() const [email, setEmail] = useState('') diff --git a/web/app/signin/LoginLogo.tsx b/web/app/signin/LoginLogo.tsx new file mode 100644 index 0000000000..0753d1f98a --- /dev/null +++ b/web/app/signin/LoginLogo.tsx @@ -0,0 +1,34 @@ +'use client' +import type { FC } from 'react' +import classNames from '@/utils/classnames' +import { useSelector } from '@/context/app-context' +import { useGlobalPublicStore } from '@/context/global-public-context' + +type LoginLogoProps = { + className?: string +} + +const LoginLogo: FC = ({ + className, +}) => { + const { systemFeatures } = useGlobalPublicStore() + const { theme } = useSelector((s) => { + return { + theme: s.theme, + } + }) + + let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` + if (systemFeatures.branding.enabled) + src = systemFeatures.branding.login_page_logo + + return ( + logo + ) +} + +export default LoginLogo diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx index 1af4082c73..4e9ac7ebf9 100644 --- a/web/app/signin/layout.tsx +++ b/web/app/signin/layout.tsx @@ -1,8 +1,13 @@ +'use client' import Header from './_header' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' +import useDocumentTitle from '@/hooks/use-document-title' -export default async function SignInLayout({ children }: any) { +export default function SignInLayout({ children }: any) { + const { systemFeatures } = useGlobalPublicStore() + useDocumentTitle('') return <>
@@ -12,9 +17,9 @@ export default async function SignInLayout({ children }: any) { {children}
-
+ {systemFeatures.branding.enabled === false &&
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. -
+
} diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index c76e088e9c..f8c973a13f 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -9,10 +9,11 @@ import MailAndPasswordAuth from './components/mail-and-password-auth' import SocialAuth from './components/social-auth' import SSOAuth from './components/sso-auth' import cn from '@/utils/classnames' -import { getSystemFeatures, invitationCheck } from '@/service/common' -import { LicenseStatus, defaultSystemFeatures } from '@/types/feature' +import { invitationCheck } from '@/service/common' +import { LicenseStatus } from '@/types/feature' import Toast from '@/app/components/base/toast' import { IS_CE_EDITION } from '@/config' +import { useGlobalPublicStore } from '@/context/global-public-context' const NormalForm = () => { const { t } = useTranslation() @@ -23,7 +24,7 @@ const NormalForm = () => { const message = decodeURIComponent(searchParams.get('message') || '') const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') const [isLoading, setIsLoading] = useState(true) - const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures) + const { systemFeatures } = useGlobalPublicStore() const [authType, updateAuthType] = useState<'code' | 'password'>('password') const [showORLine, setShowORLine] = useState(false) const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) @@ -46,12 +47,9 @@ const NormalForm = () => { message, }) } - const features = await getSystemFeatures() - const allFeatures = { ...defaultSystemFeatures, ...features } - setSystemFeatures(allFeatures) - setAllMethodsAreDisabled(!allFeatures.enable_social_oauth_login && !allFeatures.enable_email_code_login && !allFeatures.enable_email_password_login && !allFeatures.sso_enforced_for_signin) - setShowORLine((allFeatures.enable_social_oauth_login || allFeatures.sso_enforced_for_signin) && (allFeatures.enable_email_code_login || allFeatures.enable_email_password_login)) - updateAuthType(allFeatures.enable_email_password_login ? 'password' : 'code') + setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin) + setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login)) + updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code') if (isInviteLink) { const checkRes = await invitationCheck({ url: '/activate/check', @@ -65,10 +63,9 @@ const NormalForm = () => { catch (error) { console.error(error) setAllMethodsAreDisabled(true) - setSystemFeatures(defaultSystemFeatures) } finally { setIsLoading(false) } - }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink]) + }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, systemFeatures]) useEffect(() => { init() }, [init]) @@ -132,11 +129,11 @@ const NormalForm = () => { {isInviteLink ?

{t('login.join')}{workspaceName}

-

{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}

+ {!systemFeatures.branding.enabled &&

{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}

}
:

{t('login.pageTitle')}

-

{t('login.welcome')}

+ {!systemFeatures.branding.enabled &&

{t('login.welcome')}

}
}
@@ -184,29 +181,31 @@ const NormalForm = () => {
} -
- {t('login.tosDesc')} -   - {t('login.tos')} -  &  - {t('login.pp')} -
- {IS_CE_EDITION &&
- {t('login.goToInit')} -   - {t('login.setAdminAccount')} -
} + {!systemFeatures.branding.enabled && <> +
+ {t('login.tosDesc')} +   + {t('login.tos')} +  &  + {t('login.pp')} +
+ {IS_CE_EDITION &&
+ {t('login.goToInit')} +   + {t('login.setAdminAccount')} +
} + } diff --git a/web/context/access-control-store.ts b/web/context/access-control-store.ts new file mode 100644 index 0000000000..3a80d7c865 --- /dev/null +++ b/web/context/access-control-store.ts @@ -0,0 +1,34 @@ +import { create } from 'zustand' +import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' +import { AccessMode } from '@/models/access-control' +import type { App } from '@/types/app' + +type AccessControlStore = { + appId: App['id'] + setAppId: (appId: App['id']) => void + specificGroups: AccessControlGroup[] + setSpecificGroups: (specificGroups: AccessControlGroup[]) => void + specificMembers: AccessControlAccount[] + setSpecificMembers: (specificMembers: AccessControlAccount[]) => void + currentMenu: AccessMode + setCurrentMenu: (currentMenu: AccessMode) => void + selectedGroupsForBreadcrumb: AccessControlGroup[] + setSelectedGroupsForBreadcrumb: (selectedGroupsForBreadcrumb: AccessControlGroup[]) => void +} + +const useAccessControlStore = create((set) => { + return { + appId: '', + setAppId: appId => set({ appId }), + specificGroups: [], + setSpecificGroups: specificGroups => set({ specificGroups }), + specificMembers: [], + setSpecificMembers: specificMembers => set({ specificMembers }), + currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS, + setCurrentMenu: currentMenu => set({ currentMenu }), + selectedGroupsForBreadcrumb: [], + setSelectedGroupsForBreadcrumb: selectedGroupsForBreadcrumb => set({ selectedGroupsForBreadcrumb }), + } +}) + +export default useAccessControlStore diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 79cc246a93..9b95b0f1eb 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -6,17 +6,14 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec import type { FC, ReactNode } from 'react' import { fetchAppList } from '@/service/apps' import Loading from '@/app/components/base/loading' -import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common' +import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common' import type { App } from '@/types/app' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import MaintenanceNotice from '@/app/components/header/maintenance-notice' -import type { SystemFeatures } from '@/types/feature' -import { defaultSystemFeatures } from '@/types/feature' import { noop } from 'lodash-es' export type AppContextValue = { apps: App[] - systemFeatures: SystemFeatures mutateApps: VoidFunction userProfile: UserProfileResponse mutateUserProfile: VoidFunction @@ -53,7 +50,6 @@ const initialWorkspaceInfo: ICurrentWorkspace = { } const AppContext = createContext({ - systemFeatures: defaultSystemFeatures, apps: [], mutateApps: noop, userProfile: { @@ -92,10 +88,6 @@ export const AppContextProvider: FC = ({ children }) => const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace) - const { data: systemFeatures } = useSWR({ url: '/console/system-features' }, getSystemFeatures, { - fallbackData: defaultSystemFeatures, - }) - const [userProfile, setUserProfile] = useState() const [langeniusVersionInfo, setLangeniusVersionInfo] = useState(initialLangeniusVersionInfo) const [currentWorkspace, setCurrentWorkspace] = useState(initialWorkspaceInfo) @@ -129,7 +121,6 @@ export const AppContextProvider: FC = ({ children }) => return ( void + systemFeatures: SystemFeatures + setSystemFeatures: (systemFeatures: SystemFeatures) => void +} + +export const useGlobalPublicStore = create(set => ({ + isPending: true, + setIsPending: (isPending: boolean) => set(() => ({ isPending })), + systemFeatures: defaultSystemFeatures, + setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), +})) + +const GlobalPublicStoreProvider: FC = ({ + children, +}) => { + const { isPending, data } = useQuery({ + queryKey: ['systemFeatures'], + queryFn: getSystemFeatures, + }) + const { setSystemFeatures, setIsPending } = useGlobalPublicStore() + useEffect(() => { + if (data) + setSystemFeatures({ ...defaultSystemFeatures, ...data }) + }, [data, setSystemFeatures]) + + useEffect(() => { + setIsPending(isPending) + }, [isPending, setIsPending]) + + if (isPending) + return
+ return <>{children} +} +export default GlobalPublicStoreProvider diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 90af9aae0c..70c9019aca 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -48,6 +48,14 @@ type ProviderContextState = { enableEducationPlan: boolean isEducationWorkspace: boolean isEducationAccount: boolean + webappCopyrightEnabled: boolean + licenseLimit: { + workspace_members: { + size: number + limit: number + } + }, + refreshLicenseLimit: () => void } const ProviderContext = createContext({ modelProviders: [], @@ -81,6 +89,14 @@ const ProviderContext = createContext({ enableEducationPlan: false, isEducationWorkspace: false, isEducationAccount: false, + webappCopyrightEnabled: false, + licenseLimit: { + workspace_members: { + size: 0, + limit: 0, + }, + }, + refreshLicenseLimit: noop, }) export const useProviderContext = () => useContext(ProviderContext) @@ -107,6 +123,13 @@ export const ProviderContextProvider = ({ const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false) const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false) + const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false) + const [licenseLimit, setLicenseLimit] = useState({ + workspace_members: { + size: 0, + limit: 0, + }, + }) const [enableEducationPlan, setEnableEducationPlan] = useState(false) const [isEducationWorkspace, setIsEducationWorkspace] = useState(false) @@ -135,6 +158,14 @@ export const ProviderContextProvider = ({ setModelLoadBalancingEnabled(true) if (data.dataset_operator_enabled) setDatasetOperatorEnabled(true) + if (data.model_load_balancing_enabled) + setModelLoadBalancingEnabled(true) + if (data.dataset_operator_enabled) + setDatasetOperatorEnabled(true) + if (data.webapp_copyright_enabled) + setWebappCopyrightEnabled(true) + if (data.workspace_members) + setLicenseLimit({ workspace_members: data.workspace_members }) } catch (error) { console.error('Failed to fetch plan info:', error) @@ -192,6 +223,9 @@ export const ProviderContextProvider = ({ enableEducationPlan, isEducationWorkspace, isEducationAccount: isEducationAccount?.result || false, + webappCopyrightEnabled, + licenseLimit, + refreshLicenseLimit: fetchPlan, }}> {children} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index d40d96356b..b2696f7561 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -167,7 +167,7 @@ export default combine( 'sonarjs/max-lines': 'warn', // max 1000 lines 'sonarjs/no-variable-usage-before-declaration': 'error', // security - // eslint-disable-next-line sonarjs/no-hardcoded-passwords + 'sonarjs/no-hardcoded-passwords': 'off', // detect the wrong code that is not password. 'sonarjs/no-hardcoded-secrets': 'off', 'sonarjs/pseudo-random': 'off', diff --git a/web/hooks/use-document-title.spec.ts b/web/hooks/use-document-title.spec.ts new file mode 100644 index 0000000000..88239ffbdf --- /dev/null +++ b/web/hooks/use-document-title.spec.ts @@ -0,0 +1,65 @@ +import { defaultSystemFeatures } from '@/types/feature' +import { act, renderHook } from '@testing-library/react' +import useDocumentTitle from './use-document-title' +import { useGlobalPublicStore } from '@/context/global-public-context' + +jest.mock('@/service/common', () => ({ + getSystemFeatures: jest.fn(() => ({ ...defaultSystemFeatures })), +})) + +describe('title should be empty if systemFeatures is pending', () => { + act(() => { + useGlobalPublicStore.setState({ + systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, + isPending: true, + }) + }) + it('document title should be empty if set title', () => { + renderHook(() => useDocumentTitle('test')) + expect(document.title).toBe('') + }) + it('document title should be empty if not set title', () => { + renderHook(() => useDocumentTitle('')) + expect(document.title).toBe('') + }) +}) + +describe('use default branding', () => { + beforeEach(() => { + act(() => { + useGlobalPublicStore.setState({ + isPending: false, + systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, + }) + }) + }) + it('document title should be test-Dify if set title', () => { + renderHook(() => useDocumentTitle('test')) + expect(document.title).toBe('test - Dify') + }) + + it('document title should be Dify if not set title', () => { + renderHook(() => useDocumentTitle('')) + expect(document.title).toBe('Dify') + }) +}) + +describe('use specific branding', () => { + beforeEach(() => { + act(() => { + useGlobalPublicStore.setState({ + isPending: false, + systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } }, + }) + }) + }) + it('document title should be test-Test if set title', () => { + renderHook(() => useDocumentTitle('test')) + expect(document.title).toBe('test - Test') + }) + + it('document title should be Test if not set title', () => { + renderHook(() => useDocumentTitle('')) + expect(document.title).toBe('Test') + }) +}) diff --git a/web/hooks/use-document-title.ts b/web/hooks/use-document-title.ts new file mode 100644 index 0000000000..10275a196f --- /dev/null +++ b/web/hooks/use-document-title.ts @@ -0,0 +1,23 @@ +'use client' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { useFavicon, useTitle } from 'ahooks' + +export default function useDocumentTitle(title: string) { + const isPending = useGlobalPublicStore(s => s.isPending) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + const prefix = title ? `${title} - ` : '' + let titleStr = '' + let favicon = '' + if (isPending === false) { + if (systemFeatures.branding.enabled) { + titleStr = `${prefix}${systemFeatures.branding.application_title}` + favicon = systemFeatures.branding.favicon + } + else { + titleStr = `${prefix}Dify` + favicon = '/favicon.ico' + } + } + useTitle(titleStr) + useFavicon(favicon) +} diff --git a/web/hooks/use-tab-searchparams.ts b/web/hooks/use-tab-searchparams.ts index bbeb1ea8be..0c0e3b7773 100644 --- a/web/hooks/use-tab-searchparams.ts +++ b/web/hooks/use-tab-searchparams.ts @@ -1,4 +1,5 @@ -import { usePathname, useSearchParams } from 'next/navigation' +'use client' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' type UseTabSearchParamsOptions = { @@ -25,7 +26,8 @@ export const useTabSearchParams = ({ disableSearchParams = false, }: UseTabSearchParamsOptions) => { const pathnameFromHook = usePathname() - const pathName = window?.location?.pathname || pathnameFromHook + const router = useRouter() + const pathName = pathnameFromHook || window?.location?.pathname const searchParams = useSearchParams() const [activeTab, setTab] = useState( !disableSearchParams @@ -37,7 +39,7 @@ export const useTabSearchParams = ({ setTab(newActiveTab) if (disableSearchParams) return - history[`${routingBehavior}State`](null, '', `${pathName}?${searchParamName}=${newActiveTab}`) + router[`${routingBehavior}`](`${pathName}?${searchParamName}=${newActiveTab}`) } return [activeTab, setActiveTab] as const diff --git a/web/i18n/de-DE/app-overview.ts b/web/i18n/de-DE/app-overview.ts index fea278dad7..f8e934a117 100644 --- a/web/i18n/de-DE/app-overview.ts +++ b/web/i18n/de-DE/app-overview.ts @@ -30,26 +30,26 @@ const translation = { overview: { title: 'Übersicht', appInfo: { - explanation: 'Einsatzbereite AI-WebApp', + explanation: 'Einsatzbereite AI-web app', accessibleAddress: 'Öffentliche URL', preview: 'Vorschau', regenerate: 'Regenerieren', regenerateNotice: 'Möchten Sie die öffentliche URL neu generieren?', - preUseReminder: 'Bitte aktivieren Sie WebApp, bevor Sie fortfahren.', + preUseReminder: 'Bitte aktivieren Sie web app, bevor Sie fortfahren.', settings: { entry: 'Einstellungen', - title: 'WebApp-Einstellungen', - webName: 'WebApp-Name', - webDesc: 'WebApp-Beschreibung', + title: 'web app Einstellungen', + webName: 'web app Name', + webDesc: 'web app Beschreibung', webDescTip: 'Dieser Text wird auf der Clientseite angezeigt und bietet grundlegende Anleitungen zur Verwendung der Anwendung', - webDescPlaceholder: 'Geben Sie die Beschreibung der WebApp ein', + webDescPlaceholder: 'Geben Sie die Beschreibung der web app ein', language: 'Sprache', workflow: { title: 'Workflow-Schritte', show: 'Anzeigen', hide: 'Verbergen', subTitle: 'Details zum Arbeitsablauf', - showDesc: 'Ein- oder Ausblenden von Workflow-Details in der WebApp', + showDesc: 'Ein- oder Ausblenden von Workflow-Details in der web app', }, chatColorTheme: 'Chat-Farbschema', chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest', @@ -70,10 +70,10 @@ const translation = { copyrightTooltip: 'Bitte führen Sie ein Upgrade auf den Professional-Plan oder höher durch', }, sso: { - title: 'WebApp-SSO', - description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie WebApp verwenden können', + title: 'web app SSO', + description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie web app verwenden können', label: 'SSO-Authentifizierung', - tooltip: 'Wenden Sie sich an den Administrator, um WebApp-SSO zu aktivieren', + tooltip: 'Wenden Sie sich an den Administrator, um web app SSO zu aktivieren', }, modalTip: 'Einstellungen für clientseitige Web-Apps.', }, @@ -95,7 +95,7 @@ const translation = { customize: { way: 'Art', entry: 'Anpassen', - title: 'AI-WebApp anpassen', + title: 'AI-web app anpassen', explanation: 'Sie können das Frontend der Web-App an Ihre Szenarien und Stilbedürfnisse anpassen.', way1: { name: 'Forken Sie den Client-Code, ändern Sie ihn und deployen Sie ihn auf Vercel (empfohlen)', diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index d9454a2a4c..7f4b7162e3 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -167,9 +167,9 @@ const translation = { }, }, answerIcon: { - descriptionInExplore: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', - title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖', - description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll', + descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', + title: 'Verwenden Sie das web app Symbol, um es zu ersetzen 🤖', + description: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll', }, importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen', duplicate: 'Duplikat', diff --git a/web/i18n/de-DE/custom.ts b/web/i18n/de-DE/custom.ts index e86e0c2cd4..42001e0863 100644 --- a/web/i18n/de-DE/custom.ts +++ b/web/i18n/de-DE/custom.ts @@ -7,7 +7,7 @@ const translation = { des: 'Upgrade deinen Plan, um deine Marke anzupassen.', }, webapp: { - title: 'WebApp Marke anpassen', + title: 'web app Marke anpassen', removeBrand: 'Entferne Powered by Dify', changeLogo: 'Ändere Powered by Markenbild', changeLogoTip: 'SVG oder PNG Format mit einer Mindestgröße von 40x40px', diff --git a/web/i18n/en-US/app-overview.ts b/web/i18n/en-US/app-overview.ts index 0261f4cd4e..feedc32e6b 100644 --- a/web/i18n/en-US/app-overview.ts +++ b/web/i18n/en-US/app-overview.ts @@ -30,28 +30,28 @@ const translation = { overview: { title: 'Overview', appInfo: { - explanation: 'Ready-to-use AI WebApp', + explanation: 'Ready-to-use AI web app', accessibleAddress: 'Public URL', preview: 'Preview', launch: 'Launch', regenerate: 'Regenerate', regenerateNotice: 'Do you want to regenerate the public URL?', - preUseReminder: 'Please enable WebApp before continuing.', + preUseReminder: 'Please enable web app before continuing.', settings: { entry: 'Settings', title: 'Web App Settings', modalTip: 'Client-side web app settings. ', - webName: 'WebApp Name', - webDesc: 'WebApp Description', + webName: 'web app Name', + webDesc: 'web app Description', webDescTip: 'This text will be displayed on the client side, providing basic guidance on how to use the application', - webDescPlaceholder: 'Enter the description of the WebApp', + webDescPlaceholder: 'Enter the description of the web app', language: 'Language', workflow: { title: 'Workflow', subTitle: 'Workflow Details', show: 'Show', hide: 'Hide', - showDesc: 'Show or hide workflow details in WebApp', + showDesc: 'Show or hide workflow details in web app', }, chatColorTheme: 'Chat color theme', chatColorThemeDesc: 'Set the color theme of the chatbot', @@ -60,14 +60,14 @@ const translation = { invalidPrivacyPolicy: 'Invalid privacy policy link. Please use a valid link that starts with http or https', sso: { label: 'SSO Enforcement', - title: 'WebApp SSO', - description: 'All users are required to login with SSO before using WebApp', - tooltip: 'Contact the administrator to enable WebApp SSO', + title: 'web app SSO', + description: 'All users are required to login with SSO before using web app', + tooltip: 'Contact the administrator to enable web app SSO', }, more: { entry: 'Show more settings', copyright: 'Copyright', - copyrightTip: 'Display copyright information in the webapp', + copyrightTip: 'Display copyright information in the web app', copyrightTooltip: 'Please upgrade to Professional plan or above', copyRightPlaceholder: 'Enter the name of the author or organization', privacyPolicy: 'Privacy Policy', @@ -96,7 +96,7 @@ const translation = { customize: { way: 'way', entry: 'Customize', - title: 'Customize AI WebApp', + title: 'Customize AI web app', explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.', way1: { name: 'Fork the client code, modify it and deploy to Vercel (recommended)', diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index c57d6c25b5..bcfab9f57a 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -112,9 +112,9 @@ const translation = { image: 'Image', }, answerIcon: { - title: 'Use WebApp icon to replace 🤖', - description: 'Whether to use the WebApp icon to replace 🤖 in the shared application', - descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore', + title: 'Use web app icon to replace 🤖', + description: 'Whether to use the web app icon to replace 🤖 in the shared application', + descriptionInExplore: 'Whether to use the web app icon to replace 🤖 in Explore', }, switch: 'Switch to Workflow Orchestrate', switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', @@ -195,6 +195,41 @@ const translation = { modelNotSupported: 'Model not supported', modelNotSupportedTip: 'The current model does not support this feature and is automatically downgraded to prompt injection.', }, + accessControl: 'Web App Access Control', + accessItemsDescription: { + anyone: 'Anyone can access the web app', + specific: 'Only specific groups or members can access the web app', + organization: 'Anyone in the organization can access the web app', + }, + accessControlDialog: { + title: 'Web App Access Control', + description: 'Set web app access permissions', + accessLabel: 'Who has access', + accessItems: { + anyone: 'Anyone with the link', + specific: 'Specific groups or members', + organization: 'Only members within the enterprise', + }, + groups_one: '{{count}} GROUP', + groups_other: '{{count}} GROUPS', + members_one: '{{count}} MEMBER', + members_other: '{{count}} MEMBERS', + noGroupsOrMembers: 'No groups or members selected', + webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.', + operateGroupAndMember: { + searchPlaceholder: 'Search groups and members', + allMembers: 'All members', + expand: 'Expand', + noResult: 'No result', + }, + updateSuccess: 'Update successfully', + }, + publishApp: { + title: 'Who can access web app', + notSet: 'Not set', + notSetDesc: 'Currently nobody can access the web app. Please set permissions.', + }, + noAccessPermission: 'No permission to access web app', } export default translation diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index b194f6ed15..418393dde5 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -147,6 +147,8 @@ const translation = { status: 'beta', explore: 'Explore', apps: 'Studio', + appDetail: 'App Detail', + account: 'Account', plugins: 'Plugins', exploreMarketplace: 'Explore Marketplace', pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.', @@ -196,7 +198,7 @@ const translation = { account: { account: 'Account', myAccount: 'My Account', - studio: 'Dify Studio', + studio: 'Studio', avatar: 'Avatar', name: 'Name', email: 'Email', @@ -208,8 +210,8 @@ const translation = { newPassword: 'New password', confirmPassword: 'Confirm password', notEqual: 'Two passwords are different.', - langGeniusAccount: 'Dify account', - langGeniusAccountTip: 'Your Dify account and associated user data.', + langGeniusAccount: 'Account\'s data', + langGeniusAccountTip: 'The user data of your account.', editName: 'Edit Name', showAppLength: 'Show {{length}} apps', delete: 'Delete Account', @@ -657,6 +659,7 @@ const translation = { license: { expiring: 'Expiring in one day', expiring_plural: 'Expiring in {{count}} days', + unlimited: 'Unlimited', }, pagination: { perPage: 'Items per page', @@ -666,6 +669,7 @@ const translation = { browse: 'browse', supportedFormats: 'Supports PNG, JPG, JPEG, WEBP and GIF', }, + you: 'You', } export default translation diff --git a/web/i18n/en-US/custom.ts b/web/i18n/en-US/custom.ts index 408d4c55e4..d88071a018 100644 --- a/web/i18n/en-US/custom.ts +++ b/web/i18n/en-US/custom.ts @@ -7,7 +7,7 @@ const translation = { suffix: 'customize your brand.', }, webapp: { - title: 'Customize WebApp brand', + title: 'Customize web app brand', removeBrand: 'Remove Powered by Dify', changeLogo: 'Change Powered by Brand Image', changeLogoTip: 'SVG or PNG format with a minimum size of 40x40px', diff --git a/web/i18n/en-US/explore.ts b/web/i18n/en-US/explore.ts index 40e928f8da..7ae457ce9d 100644 --- a/web/i18n/en-US/explore.ts +++ b/web/i18n/en-US/explore.ts @@ -16,7 +16,7 @@ const translation = { }, }, apps: { - title: 'Explore Apps by Dify', + title: 'Explore Apps', description: 'Use these template apps instantly or customize your own apps based on the templates.', allCategories: 'Recommended', }, diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index f7d717197b..0beb631d24 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -104,6 +104,11 @@ const translation = { licenseLostTip: 'Failed to connect Dify license server. Please contact your administrator to continue using Dify.', licenseInactive: 'License Inactive', licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', + webapp: { + noLoginMethod: 'Authentication method not configured for web app', + noLoginMethodTip: 'Please contact the system admin to add an authentication method.', + disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.', + }, } export default translation diff --git a/web/i18n/es-ES/app-overview.ts b/web/i18n/es-ES/app-overview.ts index 97f32b1a7a..8413aa276a 100644 --- a/web/i18n/es-ES/app-overview.ts +++ b/web/i18n/es-ES/app-overview.ts @@ -49,7 +49,7 @@ const translation = { show: 'Mostrar', hide: 'Ocultar', subTitle: 'Detalles del flujo de trabajo', - showDesc: 'Mostrar u ocultar detalles del flujo de trabajo en WebApp', + showDesc: 'Mostrar u ocultar detalles del flujo de trabajo en web app', }, chatColorTheme: 'Tema de color del chat', chatColorThemeDesc: 'Establece el tema de color del chatbot', @@ -70,10 +70,10 @@ const translation = { copyrightTooltip: 'Actualice al plan Profesional o superior', }, sso: { - description: 'Todos los usuarios deben iniciar sesión con SSO antes de usar WebApp', - tooltip: 'Póngase en contacto con el administrador para habilitar el inicio de sesión único de WebApp', + description: 'Todos los usuarios deben iniciar sesión con SSO antes de usar web app', + tooltip: 'Póngase en contacto con el administrador para habilitar el inicio de sesión único de web app', label: 'Autenticación SSO', - title: 'WebApp SSO', + title: 'web app SSO', }, modalTip: 'Configuración de la aplicación web del lado del cliente.', }, diff --git a/web/i18n/es-ES/custom.ts b/web/i18n/es-ES/custom.ts index a3dcddef08..b7997581d4 100644 --- a/web/i18n/es-ES/custom.ts +++ b/web/i18n/es-ES/custom.ts @@ -7,7 +7,7 @@ const translation = { title: 'Actualiza tu plan', }, webapp: { - title: 'Personalizar marca de WebApp', + title: 'Personalizar marca de web app', removeBrand: 'Eliminar Powered by Dify', changeLogo: 'Cambiar Imagen de Marca Powered by', changeLogoTip: 'Formato SVG o PNG con un tamaño mínimo de 40x40px', diff --git a/web/i18n/fa-IR/app-overview.ts b/web/i18n/fa-IR/app-overview.ts index cf3368dd6f..891002b4e4 100644 --- a/web/i18n/fa-IR/app-overview.ts +++ b/web/i18n/fa-IR/app-overview.ts @@ -35,20 +35,20 @@ const translation = { preview: 'پیش‌نمایش', regenerate: 'تولید مجدد', regenerateNotice: 'آیا می‌خواهید آدرس عمومی را دوباره تولید کنید؟', - preUseReminder: 'لطفاً قبل از ادامه، WebApp را فعال کنید.', + preUseReminder: 'لطفاً قبل از ادامه، web app را فعال کنید.', settings: { entry: 'تنظیمات', - title: 'تنظیمات WebApp', - webName: 'نام WebApp', - webDesc: 'توضیحات WebApp', + title: 'تنظیمات web app', + webName: 'نام web app', + webDesc: 'توضیحات web app', webDescTip: 'این متن در سمت مشتری نمایش داده می‌شود و راهنمایی‌های اولیه در مورد نحوه استفاده از برنامه را ارائه می‌دهد', - webDescPlaceholder: 'توضیحات WebApp را وارد کنید', + webDescPlaceholder: 'توضیحات web app را وارد کنید', language: 'زبان', workflow: { title: 'مراحل کاری', show: 'نمایش', hide: 'مخفی کردن', - showDesc: 'نمایش یا پنهان کردن جزئیات گردش کار در WebApp', + showDesc: 'نمایش یا پنهان کردن جزئیات گردش کار در web app', subTitle: 'جزئیات گردش کار', }, chatColorTheme: 'تم رنگی چت', @@ -70,10 +70,10 @@ const translation = { copyrightTooltip: 'لطفا به طرح حرفه ای یا بالاتر ارتقا دهید', }, sso: { - title: 'WebApp SSO', + title: 'web app SSO', label: 'احراز هویت SSO', - description: 'همه کاربران باید قبل از استفاده از WebApp با SSO وارد شوند', - tooltip: 'برای فعال کردن WebApp SSO با سرپرست تماس بگیرید', + description: 'همه کاربران باید قبل از استفاده از web app با SSO وارد شوند', + tooltip: 'برای فعال کردن web app SSO با سرپرست تماس بگیرید', }, modalTip: 'تنظیمات برنامه وب سمت مشتری.', }, @@ -95,7 +95,7 @@ const translation = { customize: { way: 'راه', entry: 'سفارشی‌سازی', - title: 'سفارشی‌سازی WebApp AI', + title: 'سفارشی‌سازی web app AI', explanation: 'شما می‌توانید ظاهر جلویی برنامه وب را برای برآوردن نیازهای سناریو و سبک خود سفارشی کنید.', way1: { name: 'کلاینت را شاخه کنید، آن را تغییر دهید و در Vercel مستقر کنید (توصیه می‌شود)', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 5f10269e2a..d12206b485 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -169,9 +169,9 @@ const translation = { }, }, answerIcon: { - descriptionInExplore: 'آیا از نماد WebApp برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', - description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر', - title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید', + descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', + description: 'آیا از نماد web app برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر', + title: 'از نماد web app برای جایگزینی 🤖 استفاده کنید', }, mermaid: { handDrawn: 'دست کشیده شده', diff --git a/web/i18n/fr-FR/app-overview.ts b/web/i18n/fr-FR/app-overview.ts index 43cbdf499c..82db5d0be8 100644 --- a/web/i18n/fr-FR/app-overview.ts +++ b/web/i18n/fr-FR/app-overview.ts @@ -30,12 +30,12 @@ const translation = { overview: { title: 'Aperçu', appInfo: { - explanation: 'WebApp AI prête à l\'emploi', + explanation: 'web app AI prête à l\'emploi', accessibleAddress: 'URL publique', preview: 'Aperçu', regenerate: 'Regénérer', regenerateNotice: 'Voulez-vous régénérer l\'URL publique ?', - preUseReminder: 'Veuillez activer WebApp avant de continuer.', + preUseReminder: 'Veuillez activer web app avant de continuer.', settings: { entry: 'Paramètres', title: 'Paramètres de l\'application Web', @@ -48,7 +48,7 @@ const translation = { title: 'Étapes du workflow', show: 'Afficher', hide: 'Masquer', - showDesc: 'Afficher ou masquer les détails du flux de travail dans WebApp', + showDesc: 'Afficher ou masquer les détails du flux de travail dans web app', subTitle: 'Détails du flux de travail', }, chatColorTheme: 'Thème de couleur du chatbot', @@ -71,9 +71,9 @@ const translation = { }, sso: { label: 'Authentification SSO', - title: 'WebApp SSO', - tooltip: 'Contactez l’administrateur pour activer l’authentification unique WebApp', - description: 'Tous les utilisateurs doivent se connecter avec l’authentification unique avant d’utiliser WebApp', + title: 'web app SSO', + tooltip: 'Contactez l’administrateur pour activer l’authentification unique web app', + description: 'Tous les utilisateurs doivent se connecter avec l’authentification unique avant d’utiliser web app', }, modalTip: 'Paramètres de l’application web côté client.', }, diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index 16353d9962..dc10abe91b 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -165,9 +165,9 @@ const translation = { }, }, answerIcon: { - description: 'S’il faut utiliser l’icône WebApp pour remplacer 🤖 dans l’application partagée', - title: 'Utiliser l’icône WebApp pour remplacer 🤖', - descriptionInExplore: 'Utilisation de l’icône WebApp pour remplacer 🤖 dans Explore', + description: 'S’il faut utiliser l’icône web app pour remplacer 🤖 dans l’application partagée', + title: 'Utiliser l’icône web app pour remplacer 🤖', + descriptionInExplore: 'Utilisation de l’icône web app pour remplacer 🤖 dans Explore', }, importFromDSLUrlPlaceholder: 'Collez le lien DSL ici', importFromDSL: 'Importation à partir d’une DSL', diff --git a/web/i18n/fr-FR/custom.ts b/web/i18n/fr-FR/custom.ts index d2c0b9d008..ddb35cac4f 100644 --- a/web/i18n/fr-FR/custom.ts +++ b/web/i18n/fr-FR/custom.ts @@ -7,7 +7,7 @@ const translation = { title: 'Améliorez votre plan', }, webapp: { - title: 'Personnalisez la marque WebApp', + title: 'Personnalisez la marque web app', removeBrand: 'Supprimer Propulsé par Dify', changeLogo: 'Changer Propulsé par l\'Image de Marque', changeLogoTip: 'Format SVG ou PNG avec une taille minimum de 40x40px', diff --git a/web/i18n/hi-IN/app-overview.ts b/web/i18n/hi-IN/app-overview.ts index 0b514543ac..8a431e4bd9 100644 --- a/web/i18n/hi-IN/app-overview.ts +++ b/web/i18n/hi-IN/app-overview.ts @@ -53,7 +53,7 @@ const translation = { show: 'दिखाएं', hide: 'छुपाएं', subTitle: 'कार्यप्रवाह विवरण', - showDesc: 'WebApp में वर्कफ़्लो विवरण दिखाएँ या छुपाएँ', + showDesc: 'web app में वर्कफ़्लो विवरण दिखाएँ या छुपाएँ', }, chatColorTheme: 'चैटबॉट का रंग थीम', chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें', @@ -78,8 +78,8 @@ const translation = { sso: { title: 'वेबएप एसएसओ', label: 'SSO प्रमाणीकरण', - description: 'WebApp का उपयोग करने से पहले सभी उपयोगकर्ताओं को SSO के साथ लॉगिन करना आवश्यक है', - tooltip: 'WebApp SSO को सक्षम करने के लिए व्यवस्थापक से संपर्क करें', + description: 'web app का उपयोग करने से पहले सभी उपयोगकर्ताओं को SSO के साथ लॉगिन करना आवश्यक है', + tooltip: 'web app SSO को सक्षम करने के लिए व्यवस्थापक से संपर्क करें', }, modalTip: 'क्लाइंट-साइड वेब अनुप्रयोग सेटिंग्स.', }, diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index aef667ec89..ee5d77bc09 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -165,9 +165,9 @@ const translation = { }, }, answerIcon: { - title: 'बदलने 🤖 के लिए WebApp चिह्न का उपयोग करें', + title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें', descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं', - description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं', + description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए web app चिह्न का उपयोग करना है या नहीं', }, importFromDSLFile: 'डीएसएल फ़ाइल से', importFromDSLUrl: 'यूआरएल से', diff --git a/web/i18n/hi-IN/custom.ts b/web/i18n/hi-IN/custom.ts index a654fce942..a24834c873 100644 --- a/web/i18n/hi-IN/custom.ts +++ b/web/i18n/hi-IN/custom.ts @@ -7,7 +7,7 @@ const translation = { des: 'अपने ब्रांड को कस्टमाइज़ करने के लिए अपने योजना को अपग्रेड करें', }, webapp: { - title: 'WebApp का ब्रांड व्यक्तिकरण करें', + title: 'web app का ब्रांड व्यक्तिकरण करें', removeBrand: 'पावर्ड द्वारा डिफी हटाएं', changeLogo: 'पावर्ड द्वारा ब्रांड छवि बदले', changeLogoTip: 'SVG या PNG प्रारूप के साथ न्यूनतम आकार 40x40px होना चाहिए', diff --git a/web/i18n/it-IT/app-overview.ts b/web/i18n/it-IT/app-overview.ts index a8fe7f639e..2c9a3b476f 100644 --- a/web/i18n/it-IT/app-overview.ts +++ b/web/i18n/it-IT/app-overview.ts @@ -33,27 +33,27 @@ const translation = { overview: { title: 'Panoramica', appInfo: { - explanation: 'AI WebApp pronta all\'uso', + explanation: 'AI web app pronta all\'uso', accessibleAddress: 'URL Pubblico', preview: 'Anteprima', regenerate: 'Rigenera', regenerateNotice: 'Vuoi rigenerare l\'URL pubblico?', - preUseReminder: 'Attiva WebApp prima di continuare.', + preUseReminder: 'Attiva web app prima di continuare.', settings: { entry: 'Impostazioni', - title: 'Impostazioni WebApp', - webName: 'Nome WebApp', - webDesc: 'Descrizione WebApp', + title: 'Impostazioni web app', + webName: 'Nome web app', + webDesc: 'Descrizione web app', webDescTip: 'Questo testo verrà visualizzato sul lato client, fornendo una guida di base su come utilizzare l\'applicazione', - webDescPlaceholder: 'Inserisci la descrizione della WebApp', + webDescPlaceholder: 'Inserisci la descrizione della web app', language: 'Lingua', workflow: { title: 'Fasi del Workflow', show: 'Mostra', hide: 'Nascondi', subTitle: 'Dettagli del flusso di lavoro', - showDesc: 'Mostrare o nascondere i dettagli del flusso di lavoro in WebApp', + showDesc: 'Mostrare o nascondere i dettagli del flusso di lavoro in web app', }, chatColorTheme: 'Tema colore chat', chatColorThemeDesc: 'Imposta il tema colore del chatbot', @@ -74,14 +74,14 @@ const translation = { 'Inserisci il testo del disclaimer personalizzato', customDisclaimerTip: 'Il testo del disclaimer personalizzato verrà visualizzato sul lato client, fornendo informazioni aggiuntive sull\'applicazione', - copyrightTip: 'Visualizzare le informazioni sul copyright nella webapp', + copyrightTip: 'Visualizzare le informazioni sul copyright nella web app', copyrightTooltip: 'Si prega di eseguire l\'upgrade al piano Professional o superiore', }, sso: { label: 'Autenticazione SSO', - title: 'WebApp SSO', - description: 'Tutti gli utenti devono effettuare l\'accesso con SSO prima di utilizzare WebApp', - tooltip: 'Contattare l\'amministratore per abilitare l\'SSO di WebApp', + title: 'web app SSO', + description: 'Tutti gli utenti devono effettuare l\'accesso con SSO prima di utilizzare web app', + tooltip: 'Contattare l\'amministratore per abilitare l\'SSO di web app', }, modalTip: 'Impostazioni dell\'app Web lato client.', }, @@ -105,7 +105,7 @@ const translation = { customize: { way: 'modo', entry: 'Personalizza', - title: 'Personalizza AI WebApp', + title: 'Personalizza AI web app', explanation: 'Puoi personalizzare il frontend della Web App per adattarla alle tue esigenze di scenario e stile.', way1: { diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index 43fe626405..ae811571f6 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -177,9 +177,9 @@ const translation = { }, }, answerIcon: { - description: 'Se utilizzare l\'icona WebApp per la sostituzione 🤖 nell\'applicazione condivisa', - title: 'Usa l\'icona WebApp per sostituire 🤖', - descriptionInExplore: 'Se utilizzare l\'icona WebApp per sostituirla 🤖 in Esplora', + description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa', + title: 'Usa l\'icona web app per sostituire 🤖', + descriptionInExplore: 'Se utilizzare l\'icona web app per sostituirla 🤖 in Esplora', }, importFromDSLUrl: 'Dall\'URL', importFromDSLFile: 'Da file DSL', diff --git a/web/i18n/it-IT/custom.ts b/web/i18n/it-IT/custom.ts index b83769654c..c6164ce5da 100644 --- a/web/i18n/it-IT/custom.ts +++ b/web/i18n/it-IT/custom.ts @@ -7,7 +7,7 @@ const translation = { des: 'Aggiorna il tuo piano per personalizzare il tuo marchio', }, webapp: { - title: 'Personalizza il marchio WebApp', + title: 'Personalizza il marchio web app', removeBrand: 'Rimuovi Powered by Dify', changeLogo: 'Cambia immagine del marchio Powered by', changeLogoTip: 'Formato SVG o PNG con una dimensione minima di 40x40px', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index d68768661e..f0bc4ec72d 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -26,7 +26,7 @@ const translation = { appDeleteFailed: 'アプリの削除に失敗しました', join: 'コミュニティに参加する', communityIntro: - 'さまざまなチャンネルでチームメンバーや貢献者、開発者と議論します。', + 'さまざまなチャンネルでチームメンバーや貢献者、開発者と議論します。', roadmap: 'ロードマップを見る', newApp: { startFromBlank: '最初から作成', @@ -209,6 +209,41 @@ const translation = { modelNotSupported: 'モデルが対応していません', modelNotSupportedTip: '現在のモデルはこの機能に対応しておらず、自動的にプロンプトインジェクションに切り替わります。', }, + accessControl: 'Webアプリアクセス制御', + accessItemsDescription: { + anyone: '誰でも Web アプリにアクセス可能', + specific: '特定のグループまたはメンバーのみが Web アプリにアクセス可能', + organization: '組織内の誰でも Web アプリにアクセス可能', + }, + accessControlDialog: { + title: 'アクセス権限', + description: 'Webアプリのアクセス権限を設定します', + accessLabel: '誰がアクセスできますか', + accessItems: { + anyone: 'すべてのユーザー', + specific: '特定のグループメンバー', + organization: 'グループ内の全員', + }, + groups_one: '{{count}} グループ', + groups_other: '{{count}} グループ', + members_one: '{{count}} メンバー', + members_other: '{{count}} メンバー', + noGroupsOrMembers: 'グループまたはメンバーが選択されていません', + webAppSSONotEnabledTip: 'Webアプリの認証方式設定については、企業管理者へご連絡ください。', + operateGroupAndMember: { + searchPlaceholder: 'グループやメンバーを検索', + allMembers: 'すべてのメンバー', + expand: '展開', + noResult: '結果がありません', + }, + updateSuccess: '更新が成功しました', + }, + publishApp: { + title: 'Webアプリへのアクセス権', + notSet: '未設定', + notSetDesc: '現在このWebアプリには誰もアクセスできません。権限を設定してください。', + }, + noAccessPermission: 'Webアプリにアクセス権限がありません', } export default translation diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index e85f8d2228..0ced117db0 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -147,6 +147,8 @@ const translation = { status: 'ベータ版', explore: '探索', apps: 'スタジオ', + appDetail: 'アプリの詳細', + account: 'アカウント', plugins: 'プラグイン', pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT互換のAIプラグインを作成します。', datasets: 'ナレッジ', @@ -205,8 +207,8 @@ const translation = { newPassword: '新しいパスワード', confirmPassword: 'パスワードを確認', notEqual: '2つのパスワードが異なります。', - langGeniusAccount: 'Difyアカウント', - langGeniusAccountTip: 'Difyアカウントと関連するユーザーデータ。', + langGeniusAccount: 'アカウント関連データ', + langGeniusAccountTip: 'アカウントに関連するユーザーデータ。', editName: '名前を編集', showAppLength: '{{length}}アプリを表示', delete: 'アカウントを削除', @@ -214,7 +216,7 @@ const translation = { deleteConfirmTip: '確認のため、登録したメールから次の内容をに送信してください ', account: 'アカウント', myAccount: 'マイアカウント', - studio: 'Difyスタジオ', + studio: 'スタジオ', deletePrivacyLinkTip: 'お客様のデータの取り扱い方法の詳細については、当社の', deletePrivacyLink: 'プライバシーポリシー。', deleteSuccessTip: 'アカウントの削除が完了するまでに時間が必要です。すべて完了しましたら、メールでお知らせします。', @@ -662,6 +664,7 @@ const translation = { pagination: { perPage: 'ページあたりのアイテム数', }, + you: 'あなた', imageInput: { browse: 'ブラウズする', supportedFormats: 'PNG、JPG、JPEG、WEBP、およびGIFをサポートしています。', diff --git a/web/i18n/ja-JP/explore.ts b/web/i18n/ja-JP/explore.ts index 37a1f4182d..09a0748f08 100644 --- a/web/i18n/ja-JP/explore.ts +++ b/web/i18n/ja-JP/explore.ts @@ -16,7 +16,7 @@ const translation = { }, }, apps: { - title: 'Difyによるアプリの探索', + title: 'アプリを探索', description: 'これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。', allCategories: '推奨', }, diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index 7ba8047aff..974212564f 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -105,6 +105,11 @@ const translation = { licenseInactiveTip: 'ワークスペースの Dify Enterprise ライセンスが非アクティブです。Difyを引き続き使用するには、管理者に連絡してください。', licenseExpired: 'ライセンスの有効期限が切れています', licenseLostTip: 'Difyライセンスサーバーへの接続に失敗しました。続けてDifyを使用するために管理者に連絡してください。', + webapp: { + noLoginMethod: 'Webアプリに対して認証方法が構成されていません', + noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', + disabled: 'Webアプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', + }, } export default translation diff --git a/web/i18n/ko-KR/app-overview.ts b/web/i18n/ko-KR/app-overview.ts index be6e5117cf..3a9c56367c 100644 --- a/web/i18n/ko-KR/app-overview.ts +++ b/web/i18n/ko-KR/app-overview.ts @@ -72,7 +72,7 @@ const translation = { sso: { label: 'SSO 인증', title: '웹앱 SSO', - tooltip: '관리자에게 문의하여 WebApp SSO를 사용하도록 설정합니다.', + tooltip: '관리자에게 문의하여 web app SSO를 사용하도록 설정합니다.', description: '모든 사용자는 WebApp을 사용하기 전에 SSO로 로그인해야 합니다.', }, modalTip: '클라이언트 쪽 웹앱 설정.', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index aac1cb2e62..2ec2e4294c 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -161,9 +161,9 @@ const translation = { }, }, answerIcon: { - description: 'WebApp 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', - title: 'WebApp 아이콘을 사용하여 🤖', - descriptionInExplore: 'Explore에서 WebApp 아이콘을 사용하여 바꿀🤖지 여부', + description: 'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', + title: 'web app 아이콘을 사용하여 🤖', + descriptionInExplore: 'Explore에서 web app 아이콘을 사용하여 바꿀🤖지 여부', }, importFromDSL: 'DSL에서 가져오기', importFromDSLFile: 'DSL 파일에서', diff --git a/web/i18n/ko-KR/custom.ts b/web/i18n/ko-KR/custom.ts index 8b4954993f..f5bb34008d 100644 --- a/web/i18n/ko-KR/custom.ts +++ b/web/i18n/ko-KR/custom.ts @@ -7,7 +7,7 @@ const translation = { title: '플랜을 업그레이드하세요', }, webapp: { - title: 'WebApp 브랜드 사용자 정의', + title: 'web app 브랜드 사용자 정의', removeBrand: 'Powered by Dify 삭제', changeLogo: 'Powered by 브랜드 이미지 변경', changeLogoTip: '최소 크기 40x40px의 SVG 또는 PNG 형식', diff --git a/web/i18n/pl-PL/app-overview.ts b/web/i18n/pl-PL/app-overview.ts index 7459c0fe05..8ac97e6277 100644 --- a/web/i18n/pl-PL/app-overview.ts +++ b/web/i18n/pl-PL/app-overview.ts @@ -38,15 +38,15 @@ const translation = { preview: 'Podgląd', regenerate: 'Wygeneruj ponownie', regenerateNotice: 'Czy chcesz wygenerować ponownie publiczny adres URL?', - preUseReminder: 'Przed kontynuowaniem włącz aplikację WebApp.', + preUseReminder: 'Przed kontynuowaniem włącz aplikację web app.', settings: { entry: 'Ustawienia', - title: 'Ustawienia WebApp', - webName: 'Nazwa WebApp', - webDesc: 'Opis WebApp', + title: 'Ustawienia web app', + webName: 'Nazwa web app', + webDesc: 'Opis web app', webDescTip: 'Ten tekst będzie wyświetlany po stronie klienta, zapewniając podstawowe wskazówki, jak korzystać z aplikacji', - webDescPlaceholder: 'Wpisz opis WebApp', + webDescPlaceholder: 'Wpisz opis web app', language: 'Język', workflow: { title: 'Kroki przepływu pracy', diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 137fdebeae..d00bf02de1 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -173,7 +173,7 @@ const translation = { }, answerIcon: { description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.', - title: 'Użyj ikony WebApp, aby zastąpić 🤖', + title: 'Użyj ikony web app, aby zastąpić 🤖', descriptionInExplore: 'Czy używać ikony aplikacji internetowej do zastępowania 🤖 w Eksploruj', }, importFromDSL: 'Importowanie z DSL', diff --git a/web/i18n/pt-BR/app-overview.ts b/web/i18n/pt-BR/app-overview.ts index 10e47a750b..a6a76f1cf3 100644 --- a/web/i18n/pt-BR/app-overview.ts +++ b/web/i18n/pt-BR/app-overview.ts @@ -30,26 +30,26 @@ const translation = { overview: { title: 'Visão Geral', appInfo: { - explanation: 'WebApp de IA Pronta para Uso', + explanation: 'web app de IA Pronta para Uso', accessibleAddress: 'URL Pública', preview: 'Visualização', regenerate: 'Regenerar', regenerateNotice: 'Você deseja regenerar a URL pública?', - preUseReminder: 'Por favor, ative o WebApp antes de continuar.', + preUseReminder: 'Por favor, ative o web app antes de continuar.', settings: { entry: 'Configurações', - title: 'Configurações do WebApp', - webName: 'Nome do WebApp', - webDesc: 'Descrição do WebApp', + title: 'Configurações do web app', + webName: 'Nome do web app', + webDesc: 'Descrição do web app', webDescTip: 'Este texto será exibido no lado do cliente, fornecendo orientações básicas sobre como usar o aplicativo', - webDescPlaceholder: 'Insira a descrição do WebApp', + webDescPlaceholder: 'Insira a descrição do web app', language: 'Idioma', workflow: { title: 'Etapas do fluxo de trabalho', show: 'Mostrar', hide: 'Ocultar', subTitle: 'Detalhes do fluxo de trabalho', - showDesc: 'Mostrar ou ocultar detalhes do fluxo de trabalho no WebApp', + showDesc: 'Mostrar ou ocultar detalhes do fluxo de trabalho no web app', }, chatColorTheme: 'Tema de cor do chatbot', chatColorThemeDesc: 'Defina o tema de cor do chatbot', @@ -66,14 +66,14 @@ const translation = { customDisclaimer: 'Aviso Legal Personalizado', customDisclaimerPlaceholder: 'Insira o texto do aviso legal', customDisclaimerTip: 'O texto do aviso legal personalizado será exibido no lado do cliente, fornecendo informações adicionais sobre o aplicativo', - copyrightTip: 'Exibir informações de direitos autorais no webapp', + copyrightTip: 'Exibir informações de direitos autorais no web app', copyrightTooltip: 'Por favor, atualize para o plano Professional ou superior', }, sso: { - tooltip: 'Entre em contato com o administrador para habilitar o SSO do WebApp', + tooltip: 'Entre em contato com o administrador para habilitar o SSO do web app', label: 'Autenticação SSO', - title: 'WebApp SSO', - description: 'Todos os usuários devem fazer login com SSO antes de usar o WebApp', + title: 'web app SSO', + description: 'Todos os usuários devem fazer login com SSO antes de usar o web app', }, modalTip: 'Configurações do aplicativo Web do lado do cliente.', }, @@ -95,7 +95,7 @@ const translation = { customize: { way: 'modo', entry: 'Personalizar', - title: 'Personalizar WebApp de IA', + title: 'Personalizar web app de IA', explanation: 'Você pode personalizar a interface do usuário do Web App para atender às suas necessidades de cenário e estilo.', way1: { name: 'Faça um fork do código do cliente, modifique-o e implante-o no Vercel (recomendado)', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index c09c2dea7b..4670ea41fa 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -165,9 +165,9 @@ const translation = { }, }, answerIcon: { - descriptionInExplore: 'Se o ícone do WebApp deve ser usado para substituir 🤖 no Explore', - description: 'Se o ícone WebApp deve ser usado para substituir 🤖 no aplicativo compartilhado', - title: 'Use o ícone do WebApp para substituir 🤖', + descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore', + description: 'Se o ícone web app deve ser usado para substituir 🤖 no aplicativo compartilhado', + title: 'Use o ícone do web app para substituir 🤖', }, importFromDSLUrlPlaceholder: 'Cole o link DSL aqui', importFromDSLUrl: 'Do URL', diff --git a/web/i18n/pt-BR/custom.ts b/web/i18n/pt-BR/custom.ts index c1c7251f48..e167035b9b 100644 --- a/web/i18n/pt-BR/custom.ts +++ b/web/i18n/pt-BR/custom.ts @@ -7,7 +7,7 @@ const translation = { des: 'Atualize seu plano para personalizar sua marca', }, webapp: { - title: 'Personalizar marca do WebApp', + title: 'Personalizar marca do web app', removeBrand: 'Remover Powered by Dify', changeLogo: 'Alterar Imagem da Marca Powered by', changeLogoTip: 'Formato SVG ou PNG com tamanho mínimo de 40x40px', diff --git a/web/i18n/ro-RO/app-overview.ts b/web/i18n/ro-RO/app-overview.ts index 35ee79d61c..04b7540ff9 100644 --- a/web/i18n/ro-RO/app-overview.ts +++ b/web/i18n/ro-RO/app-overview.ts @@ -49,7 +49,7 @@ const translation = { show: 'Afișați', hide: 'Ascundeți', subTitle: 'Detalii despre fluxul de lucru', - showDesc: 'Afișarea sau ascunderea detaliilor fluxului de lucru în WebApp', + showDesc: 'Afișarea sau ascunderea detaliilor fluxului de lucru în web app', }, chatColorTheme: 'Tema de culoare a chatului', chatColorThemeDesc: 'Setați tema de culoare a chatbotului', @@ -71,9 +71,9 @@ const translation = { }, sso: { label: 'Autentificare SSO', - title: 'WebApp SSO', - description: 'Toți utilizatorii trebuie să se conecteze cu SSO înainte de a utiliza WebApp', - tooltip: 'Contactați administratorul pentru a activa WebApp SSO', + title: 'web app SSO', + description: 'Toți utilizatorii trebuie să se conecteze cu SSO înainte de a utiliza web app', + tooltip: 'Contactați administratorul pentru a activa web app SSO', }, modalTip: 'Setările aplicației web pe partea clientului.', }, diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index 1b1cd6c25d..1eccd0831b 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -165,9 +165,9 @@ const translation = { }, }, answerIcon: { - descriptionInExplore: 'Dacă să utilizați pictograma WebApp pentru a înlocui 🤖 în Explore', - description: 'Dacă se utilizează pictograma WebApp pentru a înlocui 🤖 în aplicația partajată', - title: 'Utilizați pictograma WebApp pentru a înlocui 🤖', + descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore', + description: 'Dacă se utilizează pictograma web app pentru a înlocui 🤖 în aplicația partajată', + title: 'Utilizați pictograma web app pentru a înlocui 🤖', }, importFromDSL: 'Import din DSL', importFromDSLUrl: 'De la URL', diff --git a/web/i18n/ro-RO/custom.ts b/web/i18n/ro-RO/custom.ts index 923ec39759..0f90836172 100644 --- a/web/i18n/ro-RO/custom.ts +++ b/web/i18n/ro-RO/custom.ts @@ -7,7 +7,7 @@ const translation = { title: 'Upgradează-ți planul', }, webapp: { - title: 'Personalizați marca WebApp', + title: 'Personalizați marca web app', removeBrand: 'Eliminați "Powered by Dify"', changeLogo: 'Schimbați imaginea mărcii "Powered by"', changeLogoTip: 'Format SVG sau PNG cu o dimensiune minimă de 40x40px', diff --git a/web/i18n/ru-RU/app-overview.ts b/web/i18n/ru-RU/app-overview.ts index 5816c37c40..ae7ec32f5a 100644 --- a/web/i18n/ru-RU/app-overview.ts +++ b/web/i18n/ru-RU/app-overview.ts @@ -58,9 +58,9 @@ const translation = { invalidPrivacyPolicy: 'Недопустимая ссылка на политику конфиденциальности. Пожалуйста, используйте действительную ссылку, начинающуюся с http или https', sso: { label: 'SSO аутентификация', - title: 'WebApp SSO', - description: 'Все пользователи должны войти в систему с помощью SSO перед использованием WebApp', - tooltip: 'Обратитесь к администратору, чтобы включить WebApp SSO', + title: 'web app SSO', + description: 'Все пользователи должны войти в систему с помощью SSO перед использованием web app', + tooltip: 'Обратитесь к администратору, чтобы включить web app SSO', }, more: { entry: 'Показать больше настроек', diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index 990457b950..300cbd36ba 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -169,9 +169,9 @@ const translation = { }, }, answerIcon: { - title: 'Использование значка WebApp для замены 🤖', - description: 'Следует ли использовать значок WebApp для замены 🤖 в общем приложении', - descriptionInExplore: 'Следует ли использовать значок WebApp для замены 🤖 в разделе "Обзор"', + title: 'Использование значка web app для замены 🤖', + description: 'Следует ли использовать значок web app для замены 🤖 в общем приложении', + descriptionInExplore: 'Следует ли использовать значок web app для замены 🤖 в разделе "Обзор"', }, mermaid: { handDrawn: 'Рисованный', diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index bff7f2aea1..b700f39f53 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -109,9 +109,9 @@ const translation = { image: 'Slika', }, answerIcon: { - title: 'Uporabite ikono WebApp za zamenjavo 🤖', - description: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v deljeni aplikaciji', - descriptionInExplore: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v razdelku Razišči', + title: 'Uporabite ikono web app za zamenjavo 🤖', + description: 'Ali uporabiti ikono web app za zamenjavo 🤖 v deljeni aplikaciji', + descriptionInExplore: 'Ali uporabiti ikono web app za zamenjavo 🤖 v razdelku Razišči', }, switch: 'Preklopi na Workflow Orchestrate', switchTipStart: 'Za vas bo ustvarjena nova kopija aplikacije, ki bo preklopila na Workflow Orchestrate. Nova kopija ne bo ', diff --git a/web/i18n/th-TH/app-overview.ts b/web/i18n/th-TH/app-overview.ts index 92b002e4a5..87eddf1f7a 100644 --- a/web/i18n/th-TH/app-overview.ts +++ b/web/i18n/th-TH/app-overview.ts @@ -30,26 +30,26 @@ const translation = { overview: { title: 'ภาพรวม', appInfo: { - explanation: 'AI WebApp พร้อมใช้งาน', + explanation: 'AI web app พร้อมใช้งาน', accessibleAddress: 'URL สาธารณะ', preview: 'ดูตัวอย่าง', regenerate: 'สร้างใหม่', regenerateNotice: 'คุณต้องการสร้าง URL สาธารณะใหม่หรือไม่', - preUseReminder: 'โปรดเปิดใช้งาน WebApp ก่อนดําเนินการต่อ', + preUseReminder: 'โปรดเปิดใช้งาน web app ก่อนดําเนินการต่อ', settings: { entry: 'การตั้งค่า', title: 'การตั้งค่าเว็บแอป', webName: 'ชื่อเว็บแอป', - webDesc: 'คําอธิบาย WebApp', + webDesc: 'คําอธิบาย web app', webDescTip: 'ข้อความนี้จะแสดงที่ฝั่งไคลเอ็นต์ โดยให้คําแนะนําพื้นฐานเกี่ยวกับวิธีการใช้แอปพลิเคชัน', - webDescPlaceholder: 'ป้อนคําอธิบายของ WebApp', + webDescPlaceholder: 'ป้อนคําอธิบายของ web app', language: 'ภาษา', workflow: { title: 'เวิร์กโฟลว์', subTitle: 'รายละเอียดเวิร์กโฟลว์', show: 'แสดง', hide: 'ซ่อน', - showDesc: 'แสดงหรือซ่อนรายละเอียดเวิร์กโฟลว์ใน WebApp', + showDesc: 'แสดงหรือซ่อนรายละเอียดเวิร์กโฟลว์ใน web app', }, chatColorTheme: 'ธีมสีแชท', chatColorThemeDesc: 'กําหนดธีมสีของแชทบอท', @@ -59,8 +59,8 @@ const translation = { sso: { label: 'การรับรองความถูกต้องของ SSO', title: 'เว็บแอป SSO', - description: 'ผู้ใช้ทุกคนต้องเข้าสู่ระบบด้วย SSO ก่อนใช้ WebApp', - tooltip: 'ติดต่อผู้ดูแลระบบเพื่อเปิดใช้ WebApp SSO', + description: 'ผู้ใช้ทุกคนต้องเข้าสู่ระบบด้วย SSO ก่อนใช้ web app', + tooltip: 'ติดต่อผู้ดูแลระบบเพื่อเปิดใช้ web app SSO', }, more: { entry: 'แสดงการตั้งค่าเพิ่มเติม', @@ -95,7 +95,7 @@ const translation = { customize: { way: 'วิธี', entry: 'ปรับแต่ง', - title: 'ปรับแต่ง AI WebApp', + title: 'ปรับแต่ง AI web app', explanation: 'คุณสามารถปรับแต่งส่วนหน้าของ Web App ให้เหมาะกับสถานการณ์และความต้องการสไตล์ของคุณได้', way1: { name: 'แยกรหัสไคลเอ็นต์ แก้ไข และปรับใช้กับ Vercel (แนะนํา)', diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index b58812bf9d..f7ddbc41eb 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -105,9 +105,9 @@ const translation = { image: 'ภาพ', }, answerIcon: { - title: 'ใช้ไอคอน WebApp เพื่อแทนที่ 🤖', - description: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่', - descriptionInExplore: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ใน Explore หรือไม่', + title: 'ใช้ไอคอน web app เพื่อแทนที่ 🤖', + description: 'จะใช้ไอคอน web app เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่', + descriptionInExplore: 'จะใช้ไอคอน web app เพื่อแทนที่🤖ใน Explore หรือไม่', }, switch: 'เปลี่ยนไปใช้ Workflow Orchestrate', switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration', diff --git a/web/i18n/th-TH/custom.ts b/web/i18n/th-TH/custom.ts index c5ae3e79db..41a0441988 100644 --- a/web/i18n/th-TH/custom.ts +++ b/web/i18n/th-TH/custom.ts @@ -7,7 +7,7 @@ const translation = { title: 'อัปเกรดแผนของคุณ', }, webapp: { - title: 'ปรับแต่งแบรนด์ WebApp', + title: 'ปรับแต่งแบรนด์ web app', removeBrand: 'ลบ ขับเคลื่อนโดย Dify', changeLogo: 'การเปลี่ยนแปลงที่ขับเคลื่อนโดยภาพลักษณ์ของแบรนด์', changeLogoTip: 'รูปแบบ SVG หรือ PNG ที่มีขนาดขั้นต่ํา 40x40px', diff --git a/web/i18n/tr-TR/app-overview.ts b/web/i18n/tr-TR/app-overview.ts index f7203e2f59..f6c16553f1 100644 --- a/web/i18n/tr-TR/app-overview.ts +++ b/web/i18n/tr-TR/app-overview.ts @@ -30,25 +30,25 @@ const translation = { overview: { title: 'Genel Bakış', appInfo: { - explanation: 'Kullanıma hazır AI WebApp', + explanation: 'Kullanıma hazır AI web app', accessibleAddress: 'Genel URL', preview: 'Önizleme', regenerate: 'Yeniden Oluştur', regenerateNotice: 'Genel URL\'yi yeniden oluşturmak istiyor musunuz?', - preUseReminder: 'Devam etmeden önce WebApp\'i etkinleştirin.', + preUseReminder: 'Devam etmeden önce web app\'i etkinleştirin.', settings: { entry: 'Ayarlar', - title: 'WebApp Ayarları', - webName: 'WebApp İsmi', - webDesc: 'WebApp Açıklaması', + title: 'web app Ayarları', + webName: 'web app İsmi', + webDesc: 'web app Açıklaması', webDescTip: 'Bu metin, uygulamanın nasıl kullanılacağına dair temel açıklamalar sağlar ve istemci tarafında görüntülenir', - webDescPlaceholder: 'WebApp\'in açıklamasını girin', + webDescPlaceholder: 'web app\'in açıklamasını girin', language: 'Dil', workflow: { title: 'Workflow Adımları', show: 'Göster', hide: 'Gizle', - showDesc: 'WebApp\'te iş akışı ayrıntılarını gösterme veya gizleme', + showDesc: 'web app\'te iş akışı ayrıntılarını gösterme veya gizleme', subTitle: 'İş Akışı Detayları', }, chatColorTheme: 'Sohbet renk teması', @@ -70,10 +70,10 @@ const translation = { copyrightTooltip: 'Lütfen Profesyonel plana veya daha yüksek bir plana yükseltin', }, sso: { - title: 'WebApp SSO\'su', - tooltip: 'WebApp SSO\'yu etkinleştirmek için yöneticiyle iletişime geçin', + title: 'web app SSO\'su', + tooltip: 'web app SSO\'yu etkinleştirmek için yöneticiyle iletişime geçin', label: 'SSO Kimlik Doğrulaması', - description: 'Tüm kullanıcıların WebApp\'i kullanmadan önce SSO ile oturum açmaları gerekir', + description: 'Tüm kullanıcıların web app\'i kullanmadan önce SSO ile oturum açmaları gerekir', }, modalTip: 'İstemci tarafı web uygulaması ayarları.', }, @@ -95,7 +95,7 @@ const translation = { customize: { way: 'yol', entry: 'Özelleştir', - title: 'AI WebApp\'i Özelleştirin', + title: 'AI web app\'i Özelleştirin', explanation: 'Web Uygulamasının ön yüzünü senaryo ve stil ihtiyaçlarınıza uygun şekilde özelleştirebilirsiniz.', way1: { name: 'İstemci kodunu forklayarak değiştirin ve Vercel\'e dağıtın (önerilen)', diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 0e27f84582..f963044dea 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -165,9 +165,9 @@ const translation = { }, }, answerIcon: { - descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 WebApp simgesinin kullanılıp kullanılmayacağı', - title: 'Değiştirmek 🤖 için WebApp simgesini kullanın', - description: 'Paylaşılan uygulamada değiştirmek 🤖 için WebApp simgesinin kullanılıp kullanılmayacağı', + descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 web app simgesinin kullanılıp kullanılmayacağı', + title: 'Değiştirmek 🤖 için web app simgesini kullanın', + description: 'Paylaşılan uygulamada değiştirmek 🤖 için web app simgesinin kullanılıp kullanılmayacağı', }, mermaid: { handDrawn: 'Elle çizilmiş', diff --git a/web/i18n/tr-TR/custom.ts b/web/i18n/tr-TR/custom.ts index 15c4ff59ca..83135a1787 100644 --- a/web/i18n/tr-TR/custom.ts +++ b/web/i18n/tr-TR/custom.ts @@ -7,7 +7,7 @@ const translation = { title: 'Planınızı yükseltin', }, webapp: { - title: 'WebApp markasını özelleştir', + title: 'web app markasını özelleştir', removeBrand: 'Powered by Dify\'i kaldır', changeLogo: 'Powered by Brand Resmini Değiştir', changeLogoTip: 'SVG veya PNG formatında, en az 40x40px boyutunda', diff --git a/web/i18n/uk-UA/app-overview.ts b/web/i18n/uk-UA/app-overview.ts index 002ab5da96..1a95b47abd 100644 --- a/web/i18n/uk-UA/app-overview.ts +++ b/web/i18n/uk-UA/app-overview.ts @@ -70,9 +70,9 @@ const translation = { copyrightTooltip: 'Будь ласка, перейдіть на тарифний план «Professional» або вище', }, sso: { - title: 'Єдиний вхід для WebApp', - description: 'Усі користувачі повинні увійти в систему за допомогою єдиного входу перед використанням WebApp', - tooltip: 'Зверніться до адміністратора, щоб увімкнути єдиний вхід WebApp', + title: 'Єдиний вхід для web app', + description: 'Усі користувачі повинні увійти в систему за допомогою єдиного входу перед використанням web app', + tooltip: 'Зверніться до адміністратора, щоб увімкнути єдиний вхід web app', label: 'Автентифікація за допомогою єдиного входу', }, modalTip: 'Налаштування веб-додатку на стороні клієнта.', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 09df6bf413..a90fcd9a3a 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -165,8 +165,8 @@ const translation = { }, }, answerIcon: { - title: 'Використовуйте піктограму WebApp для заміни 🤖', - description: 'Чи слід використовувати піктограму WebApp для заміни 🤖 у спільній програмі', + title: 'Використовуйте піктограму web app для заміни 🤖', + description: 'Чи слід використовувати піктограму web app для заміни 🤖 у спільній програмі', descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді', }, importFromDSLUrl: 'З URL', diff --git a/web/i18n/uk-UA/custom.ts b/web/i18n/uk-UA/custom.ts index 1eba3f14c8..1b7a4c0abd 100644 --- a/web/i18n/uk-UA/custom.ts +++ b/web/i18n/uk-UA/custom.ts @@ -7,7 +7,7 @@ const translation = { des: 'Оновіть свій план, щоб налаштувати свій бренд', }, webapp: { - title: 'Налаштувати бренд для WebApp', + title: 'Налаштувати бренд для web app', removeBrand: 'Видалити Powered by Dify', changeLogo: 'Змінити зображення бренду "Powered by"', changeLogoTip: 'Формат SVG або PNG з мінімальним розміром 40x40 пікселів', diff --git a/web/i18n/vi-VN/app-overview.ts b/web/i18n/vi-VN/app-overview.ts index a0b7bd006d..34f3735beb 100644 --- a/web/i18n/vi-VN/app-overview.ts +++ b/web/i18n/vi-VN/app-overview.ts @@ -48,7 +48,7 @@ const translation = { title: 'Các bước quy trình', show: 'Hiển thị', hide: 'Ẩn', - showDesc: 'Hiển thị hoặc ẩn chi tiết dòng công việc trong WebApp', + showDesc: 'Hiển thị hoặc ẩn chi tiết dòng công việc trong web app', subTitle: 'Chi tiết quy trình làm việc', }, chatColorTheme: 'Giao diện màu trò chuyện', @@ -71,8 +71,8 @@ const translation = { }, sso: { title: 'SSO ứng dụng web', - description: 'Tất cả người dùng được yêu cầu đăng nhập bằng SSO trước khi sử dụng WebApp', - tooltip: 'Liên hệ với quản trị viên để bật SSO WebApp', + description: 'Tất cả người dùng được yêu cầu đăng nhập bằng SSO trước khi sử dụng web app', + tooltip: 'Liên hệ với quản trị viên để bật SSO web app', label: 'Xác thực SSO', }, modalTip: 'Cài đặt ứng dụng web phía máy khách.', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index aacfc6419b..142bf8bb89 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -165,9 +165,9 @@ const translation = { }, }, answerIcon: { - description: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong ứng dụng được chia sẻ hay không', - descriptionInExplore: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong Khám phá hay không', - title: 'Sử dụng biểu tượng WebApp để thay thế 🤖', + description: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong ứng dụng được chia sẻ hay không', + descriptionInExplore: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong Khám phá hay không', + title: 'Sử dụng biểu tượng web app để thay thế 🤖', }, importFromDSLFile: 'Từ tệp DSL', importFromDSL: 'Nhập từ DSL', diff --git a/web/i18n/vi-VN/custom.ts b/web/i18n/vi-VN/custom.ts index 6f9a4720c4..4122b5d35c 100644 --- a/web/i18n/vi-VN/custom.ts +++ b/web/i18n/vi-VN/custom.ts @@ -7,7 +7,7 @@ const translation = { title: 'Nâng cấp gói của bạn', }, webapp: { - title: 'Tùy chỉnh thương hiệu WebApp', + title: 'Tùy chỉnh thương hiệu web app', removeBrand: 'Xóa "Được hỗ trợ bởi Dify"', changeLogo: 'Thay đổi logo "Được hỗ trợ bởi"', changeLogoTip: 'Định dạng SVG hoặc PNG với kích thước tối thiểu 40x40px', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index c2c659b41f..af221a926c 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -214,7 +214,7 @@ const translation = { modalTitle: '图片上传设置', }, bar: { - empty: '开启功能增强 webapp 用户体验', + empty: '开启功能增强 web app 用户体验', enableText: '功能已开启', manage: '管理', }, diff --git a/web/i18n/zh-Hans/app-log.ts b/web/i18n/zh-Hans/app-log.ts index 8c7ad62b4f..4c18157876 100644 --- a/web/i18n/zh-Hans/app-log.ts +++ b/web/i18n/zh-Hans/app-log.ts @@ -29,7 +29,7 @@ const translation = { noOutput: '无输出', element: { title: '这里有人吗', - content: '在这里观测和标注最终用户和 AI 应用程序之间的交互,以不断提高 AI 的准确性。您可以试试 WebApp 或分享出去,然后返回此页面。', + content: '在这里观测和标注最终用户和 AI 应用程序之间的交互,以不断提高 AI 的准确性。您可以试试 web app 或分享出去,然后返回此页面。', }, }, }, diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index aebee63ed6..1486f9b4c4 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/web/i18n/zh-Hans/app-overview.ts @@ -30,7 +30,7 @@ const translation = { overview: { title: '概览', appInfo: { - explanation: '开箱即用的 AI WebApp', + explanation: '开箱即用的 AI web app', accessibleAddress: '公开访问 URL', preview: '预览', launch: '启动', @@ -39,19 +39,19 @@ const translation = { preUseReminder: '使用前请先打开开关', settings: { entry: '设置', - title: 'WebApp 设置', - modalTip: '客户端 WebApp 设置。', - webName: 'WebApp 名称', - webDesc: 'WebApp 描述', + title: 'web app 设置', + modalTip: '客户端 web app 设置。', + webName: 'web app 名称', + webDesc: 'web app 描述', webDescTip: '以下文字将展示在客户端中,对应用进行说明和使用上的基本引导', - webDescPlaceholder: '请输入 WebApp 的描述', + webDescPlaceholder: '请输入 web app 的描述', language: '语言', workflow: { title: '工作流', subTitle: '工作流详情', show: '显示', hide: '隐藏', - showDesc: '在 WebApp 中展示或者隐藏工作流详情', + showDesc: '在 web app 中展示或者隐藏工作流详情', }, chatColorTheme: '聊天颜色主题', chatColorThemeDesc: '设置聊天机器人的颜色主题', @@ -60,14 +60,14 @@ const translation = { invalidPrivacyPolicy: '无效的隐私政策链接,请使用以 http 或 https 开头的有效链接', sso: { label: '单点登录认证', - title: 'WebApp SSO 认证', + title: 'web app SSO 认证', description: '启用后,所有用户都需要先进行 SSO 认证才能访问', - tooltip: '联系管理员以开启 WebApp SSO 认证', + tooltip: '联系管理员以开启 web app SSO 认证', }, more: { entry: '展示更多设置', copyright: '版权', - copyrightTip: '在 WebApp 中展示版权信息', + copyrightTip: '在 web app 中展示版权信息', copyrightTooltip: '请升级到专业版或者更高', copyRightPlaceholder: '请输入作者或组织名称', privacyPolicy: '隐私政策', @@ -96,7 +96,7 @@ const translation = { customize: { way: '方法', entry: '定制化', - title: '定制化 AI WebApp', + title: '定制化 AI web app', explanation: '你可以定制化 Web App 前端以符合你的情景与风格需求', way1: { name: 'Fork 客户端代码修改后部署到 Vercel(推荐)', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index c1723fcab9..a634394cfb 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -113,9 +113,9 @@ const translation = { image: '图片', }, answerIcon: { - title: '使用 WebApp 图标替换 🤖', - description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖', - descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖', + title: '使用 web app 图标替换 🤖', + description: '是否使用 web app 图标替换分享的应用界面中的 🤖', + descriptionInExplore: '是否使用 web app 图标替换 Explore 界面中的 🤖', }, switch: '迁移为工作流编排', switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', @@ -196,6 +196,46 @@ const translation = { modelNotSupported: '模型不支持', modelNotSupportedTip: '当前模型不支持此功能,将自动降级为提示注入。', }, + accessControl: 'Web 应用访问控制', + accessItemsDescription: { + anyone: '任何人可以访问 web 应用', + specific: '特定组或成员可以访问 web 应用', + organization: '组织内任何人可以访问 web 应用', + }, + accessControlDialog: { + title: 'Web 应用访问权限', + description: '设置 web 应用访问权限。', + accessLabel: '谁可以访问', + accessItemsDescription: { + anyone: '任何人可以访问 web 应用', + specific: '特定组或成员可以访问 web 应用', + organization: '组织内任何人可以访问 web 应用', + }, + accessItems: { + anyone: '任何人', + specific: '特定组或成员', + organization: '组织内任何人', + }, + groups_one: '{{count}} 个组', + groups_other: '{{count}} 个组', + members_one: '{{count}} 个成员', + members_other: '{{count}} 个成员', + noGroupsOrMembers: '未选择分组或成员', + webAppSSONotEnabledTip: '请联系企业管理员配置 web 应用的身份认证方式。', + operateGroupAndMember: { + searchPlaceholder: '搜索组或成员', + allMembers: '所有成员', + expand: '展开', + noResult: '没有结果', + }, + updateSuccess: '更新成功', + }, + publishApp: { + title: '谁可以访问 web 应用', + notSet: '未设置', + notSetDesc: '当前任何人都无法访问 Web 应用。请设置访问权限。', + }, + noAccessPermission: '没有权限访问 web 应用', } export default translation diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 9ed961feab..f3a7e09ab7 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -147,6 +147,8 @@ const translation = { status: 'beta', explore: '探索', apps: '工作室', + appDetail: '应用详情', + account: '账户', plugins: '插件', exploreMarketplace: '探索 Marketplace', pluginsTips: '集成第三方插件或创建与 ChatGPT 兼容的 AI 插件。', @@ -196,7 +198,7 @@ const translation = { account: { account: '账户', myAccount: '我的账户', - studio: 'Dify 工作室', + studio: '工作室', avatar: '头像', name: '用户名', email: '邮箱', @@ -208,8 +210,8 @@ const translation = { newPassword: '新密码', notEqual: '两个密码不相同', confirmPassword: '确认密码', - langGeniusAccount: 'Dify 账号', - langGeniusAccountTip: '您的 Dify 账号和相关的用户数据。', + langGeniusAccount: '账号关联数据', + langGeniusAccountTip: '您的账号相关的用户数据。', editName: '编辑名字', showAppLength: '显示 {{length}} 个应用', delete: '删除账户', @@ -657,6 +659,7 @@ const translation = { license: { expiring: '许可证还有 1 天到期', expiring_plural: '许可证还有 {{count}} 天到期', + unlimited: '无限制', }, pagination: { perPage: '每页显示', @@ -666,6 +669,7 @@ const translation = { browse: '浏览', supportedFormats: '支持PNG、JPG、JPEG、WEBP和GIF格式', }, + you: '你', } export default translation diff --git a/web/i18n/zh-Hans/custom.ts b/web/i18n/zh-Hans/custom.ts index 4bec191a60..d388c285ff 100644 --- a/web/i18n/zh-Hans/custom.ts +++ b/web/i18n/zh-Hans/custom.ts @@ -7,7 +7,7 @@ const translation = { suffix: '定制您的品牌。', }, webapp: { - title: '定制 WebApp 品牌', + title: '定制 web app 品牌', removeBrand: '移除 Powered by Dify', changeLogo: '更改 Powered by Brand 图片', changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 40x40px', diff --git a/web/i18n/zh-Hans/explore.ts b/web/i18n/zh-Hans/explore.ts index 896a80ab25..7f16cd32f2 100644 --- a/web/i18n/zh-Hans/explore.ts +++ b/web/i18n/zh-Hans/explore.ts @@ -16,7 +16,7 @@ const translation = { }, }, apps: { - title: '探索 Dify 的应用', + title: '探索应用', description: '使用这些模板应用程序,或根据模板自定义您自己的应用程序。', allCategories: '推荐', }, diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index 7f64c954b1..e2a958ea05 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -105,6 +105,11 @@ const translation = { licenseLostTip: '无法连接 Dify 许可证服务器,请联系管理员以继续使用 Dify。', licenseInactive: '许可证未激活', licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', + webapp: { + noLoginMethod: 'Web 应用未配置身份认证方式', + noLoginMethodTip: '请联系系统管理员添加身份认证方式', + disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', + }, } export default translation diff --git a/web/i18n/zh-Hant/app-log.ts b/web/i18n/zh-Hant/app-log.ts index 3813c8bde0..fcedbbe53d 100644 --- a/web/i18n/zh-Hant/app-log.ts +++ b/web/i18n/zh-Hant/app-log.ts @@ -29,7 +29,7 @@ const translation = { noOutput: '無輸出', element: { title: '這裡有人嗎', - content: '在這裡觀測和標註終端使用者和 AI 應用程式之間的互動,以不斷提高 AI 的準確性。您可以試試 WebApp 或分享出去,然後返回此頁面。', + content: '在這裡觀測和標註終端使用者和 AI 應用程式之間的互動,以不斷提高 AI 的準確性。您可以試試 web app 或分享出去,然後返回此頁面。', }, }, }, diff --git a/web/i18n/zh-Hant/app-overview.ts b/web/i18n/zh-Hant/app-overview.ts index 609c3a5dda..2537e0d4c7 100644 --- a/web/i18n/zh-Hant/app-overview.ts +++ b/web/i18n/zh-Hant/app-overview.ts @@ -30,7 +30,7 @@ const translation = { overview: { title: '概覽', appInfo: { - explanation: '開箱即用的 AI WebApp', + explanation: '開箱即用的 AI web app', accessibleAddress: '公開訪問 URL', preview: '預覽', regenerate: '重新生成', @@ -38,18 +38,18 @@ const translation = { preUseReminder: '使用前請先開啟開關', settings: { entry: '設定', - title: 'WebApp 設定', - webName: 'WebApp 名稱', - webDesc: 'WebApp 描述', + title: 'web app 設定', + webName: 'web app 名稱', + webDesc: 'web app 描述', webDescTip: '以下文字將展示在客戶端中,對應用進行說明和使用上的基本引導', - webDescPlaceholder: '請輸入 WebApp 的描述', + webDescPlaceholder: '請輸入 web app 的描述', language: '語言', workflow: { title: '工作流程步驟', show: '展示', hide: '隱藏', subTitle: '工作流詳細資訊', - showDesc: '在 WebApp 中顯示或隱藏工作流詳細資訊', + showDesc: '在 web app 中顯示或隱藏工作流詳細資訊', }, chatColorTheme: '聊天顏色主題', chatColorThemeDesc: '設定聊天機器人的顏色主題', @@ -70,9 +70,9 @@ const translation = { copyrightTooltip: '請升級至專業計劃或以上', }, sso: { - description: '所有使用者在使用 WebApp 之前都需要使用 SSO 登錄', - title: 'WebApp SSO', - tooltip: '聯繫管理員以啟用 WebApp SSO', + description: '所有使用者在使用 web app 之前都需要使用 SSO 登錄', + title: 'web app SSO', + tooltip: '聯繫管理員以啟用 web app SSO', label: 'SSO 身份驗證', }, modalTip: '用戶端 Web 應用程式設置。', @@ -95,7 +95,7 @@ const translation = { customize: { way: '方法', entry: '定製化', - title: '定製化 AI WebApp', + title: '定製化 AI web app', explanation: '你可以定製化 Web App 前端以符合你的情景與風格需求', way1: { name: 'Fork 客戶端程式碼修改後部署到 Vercel(推薦)', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index f49adc84bc..6b3868745f 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -164,9 +164,9 @@ const translation = { }, }, answerIcon: { - descriptionInExplore: '是否使用 WebApp 圖示在 Explore 中取代 🤖', - title: '使用 WebApp 圖示取代 🤖', - description: '是否在共享應用程式中使用 WebApp 圖示進行取代 🤖', + descriptionInExplore: '是否使用 web app 圖示在 Explore 中取代 🤖', + title: '使用 web app 圖示取代 🤖', + description: '是否在共享應用程式中使用 web app 圖示進行取代 🤖', }, importFromDSLUrl: '寄件者 URL', importFromDSL: '從 DSL 導入', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 84b7bc53c2..266880b940 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -137,6 +137,8 @@ const translation = { status: 'beta', explore: '探索', apps: '工作室', + appDetail: '應用詳情', + account: '我的帳戶', plugins: '外掛', pluginsTips: '整合第三方外掛或建立與 ChatGPT 相容的 AI 外掛。', datasets: '知識庫', @@ -187,8 +189,8 @@ const translation = { newPassword: '新密碼', notEqual: '兩個密碼不相同', confirmPassword: '確認密碼', - langGeniusAccount: 'Dify 賬號', - langGeniusAccountTip: '您的 Dify 賬號和相關的使用者資料。', + langGeniusAccount: '賬號数据', + langGeniusAccountTip: '您的賬號和相關的使用者資料。', editName: '編輯名字', showAppLength: '顯示 {{length}} 個應用', delete: '刪除帳戶', @@ -196,7 +198,7 @@ const translation = { deleteConfirmTip: '請將以下內容從您的註冊電子郵件發送至 ', account: '帳戶', myAccount: '我的帳戶', - studio: 'Dify 工作室', + studio: '工作室', deletePrivacyLinkTip: '有關我們如何處理您的數據的更多資訊,請參閱我們的', deletePrivacyLink: '隱私策略。', deleteSuccessTip: '您的帳戶需要時間才能完成刪除。完成後,我們會給您發送電子郵件。', diff --git a/web/i18n/zh-Hant/custom.ts b/web/i18n/zh-Hant/custom.ts index bda56657f3..08dd332a8c 100644 --- a/web/i18n/zh-Hant/custom.ts +++ b/web/i18n/zh-Hant/custom.ts @@ -7,7 +7,7 @@ const translation = { title: '升級您的計劃', }, webapp: { - title: '定製 WebApp 品牌', + title: '定製 web app 品牌', removeBrand: '移除 Powered by Dify', changeLogo: '更改 Powered by Brand 圖片', changeLogoTip: 'SVG 或 PNG 格式,最小尺寸為 40x40px', diff --git a/web/i18n/zh-Hant/explore.ts b/web/i18n/zh-Hant/explore.ts index c0f4a51e30..7ff61a39bc 100644 --- a/web/i18n/zh-Hant/explore.ts +++ b/web/i18n/zh-Hant/explore.ts @@ -16,7 +16,7 @@ const translation = { }, }, apps: { - title: '探索 Dify 的應用', + title: '探索應用', description: '使用這些模板應用程式,或根據模板自定義您自己的應用程式。', allCategories: '推薦', }, diff --git a/web/middleware.ts b/web/middleware.ts index c5c6938113..3fee535ea4 100644 --- a/web/middleware.ts +++ b/web/middleware.ts @@ -6,7 +6,7 @@ const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* http const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => { // prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking // Chatbot page should be allowed to be embedded in iframe. It's a feature - if (process.env.NEXT_PUBLIC_ALLOW_EMBED !== 'true' && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion')) + if (process.env.NEXT_PUBLIC_ALLOW_EMBED !== 'true' && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion') && !pathname.startsWith('/webapp-signin')) response.headers.set('X-Frame-Options', 'DENY') return response diff --git a/web/models/access-control.ts b/web/models/access-control.ts new file mode 100644 index 0000000000..8ad9cc6491 --- /dev/null +++ b/web/models/access-control.ts @@ -0,0 +1,29 @@ +export enum SubjectType { + GROUP = 'group', + ACCOUNT = 'account', +} + +export enum AccessMode { + PUBLIC = 'public', + SPECIFIC_GROUPS_MEMBERS = 'private', + ORGANIZATION = 'private_all', +} + +export type AccessControlGroup = { + id: 'string' + name: 'string' + groupSize: 5 +} + +export type AccessControlAccount = { + id: 'string' + name: 'string' + email: 'string' + avatar: 'string' + avatarUrl: 'string' +} + +export type SubjectGroup = { subjectId: string; subjectType: SubjectType; groupData: AccessControlGroup } +export type SubjectAccount = { subjectId: string; subjectType: SubjectType; accountData: AccessControlAccount } + +export type Subject = SubjectGroup | SubjectAccount diff --git a/web/models/app.ts b/web/models/app.ts index d83045570a..b10ced703a 100644 --- a/web/models/app.ts +++ b/web/models/app.ts @@ -1,7 +1,64 @@ import type { LangFuseConfig, LangSmithConfig, OpikConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' -import type { App, AppSSO, AppTemplate, SiteConfig } from '@/types/app' +import type { App, AppTemplate, SiteConfig } from '@/types/app' import type { Dependency } from '@/app/components/plugins/types' +/* export type App = { + id: string + name: string + description: string + mode: AppMode + enable_site: boolean + enable_api: boolean + api_rpm: number + api_rph: number + is_demo: boolean + model_config: AppModelConfig + providers: Array<{ provider: string; token_is_set: boolean }> + site: SiteConfig + created_at: string +} + +export type AppModelConfig = { + provider: string + model_id: string + configs: { + prompt_template: string + prompt_variables: Array + completion_params: CompletionParam + } +} + +export type PromptVariable = { + key: string + name: string + description: string + type: string | number + default: string + options: string[] +} + +export type CompletionParam = { + max_tokens: number + temperature: number + top_p: number + echo: boolean + stop: string[] + presence_penalty: number + frequency_penalty: number +} + +export type SiteConfig = { + access_token: string + title: string + author: string + support_email: string + default_language: string + customize_domain: string + theme: string + customize_token_strategy: 'must' | 'allow' | 'not_allow' + prompt_public: boolean +} */ + export enum DSLImportMode { YAML_CONTENT = 'yaml-content', YAML_URL = 'yaml-url', @@ -35,8 +92,6 @@ export type DSLImportResponse = { leaked_dependencies: Dependency[] } -export type AppSSOResponse = { enabled: AppSSO['enable_sso'] } - export type AppTemplatesResponse = { data: AppTemplate[] } diff --git a/web/service/access-control.ts b/web/service/access-control.ts new file mode 100644 index 0000000000..865909d2f9 --- /dev/null +++ b/web/service/access-control.ts @@ -0,0 +1,90 @@ +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { get, post } from './base' +import { getAppAccessMode, getUserCanAccess } from './share' +import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' +import type { App } from '@/types/app' + +const NAME_SPACE = 'access-control' + +export const useAppWhiteListSubjects = (appId: string | undefined, enabled: boolean) => { + return useQuery({ + queryKey: [NAME_SPACE, 'app-whitelist-subjects', appId], + queryFn: () => get<{ groups: AccessControlGroup[]; members: AccessControlAccount[] }>(`/enterprise/webapp/app/subjects?appId=${appId}`), + enabled: !!appId && enabled, + staleTime: 0, + gcTime: 0, + }) +} + +type SearchResults = { + currPage: number + totalPages: number + subjects: Subject[] + hasMore: boolean +} + +export const useSearchForWhiteListCandidates = (query: { keyword?: string; groupId?: AccessControlGroup['id']; resultsPerPage?: number }, enabled: boolean) => { + return useInfiniteQuery({ + queryKey: [NAME_SPACE, 'app-whitelist-candidates', query], + queryFn: ({ pageParam }) => { + const params = new URLSearchParams() + Object.keys(query).forEach((key) => { + const typedKey = key as keyof typeof query + if (query[typedKey]) + params.append(key, `${query[typedKey]}`) + }) + params.append('pageNumber', `${pageParam}`) + return get(`/enterprise/webapp/app/subject/search?${new URLSearchParams(params).toString()}`) + }, + initialPageParam: 1, + getNextPageParam: (lastPage) => { + if (lastPage.hasMore) + return lastPage.currPage + 1 + return undefined + }, + gcTime: 0, + staleTime: 0, + enabled, + }) +} + +type UpdateAccessModeParams = { + appId: App['id'] + subjects?: Pick[] + accessMode: AccessMode +} + +export const useUpdateAccessMode = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationKey: [NAME_SPACE, 'update-access-mode'], + mutationFn: (params: UpdateAccessModeParams) => { + return post('/enterprise/webapp/app/access-mode', { body: params }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [NAME_SPACE, 'app-whitelist-subjects'], + }) + }, + }) +} + +export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { + return useQuery({ + queryKey: [NAME_SPACE, 'app-access-mode', appId], + queryFn: () => getAppAccessMode(appId!, isInstalledApp), + enabled: !!appId && enabled, + staleTime: 0, + gcTime: 0, + }) +} + +export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { + return useQuery({ + queryKey: [NAME_SPACE, 'user-can-access-app', appId], + queryFn: () => getUserCanAccess(appId!, isInstalledApp), + enabled: !!appId && enabled, + staleTime: 0, + gcTime: 0, + }) +} diff --git a/web/service/apps.ts b/web/service/apps.ts index 3f7ec7b548..d87a98412e 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -1,6 +1,6 @@ import type { Fetcher } from 'swr' import { del, get, patch, post, put } from './base' -import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppSSOResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' +import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' import type { CommonResponse } from '@/models/common' import type { AppIconType, AppMode, ModelConfig } from '@/types/app' import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' @@ -13,13 +13,6 @@ export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => { return get(`${url}/${id}`) } -export const fetchAppSSO = async ({ appId }: { appId: string }) => { - return get(`/enterprise/app-setting/sso?appID=${appId}`) -} -export const updateAppSSO = async ({ id, enabled }: { id: string; enabled: boolean }) => { - return post('/enterprise/app-setting/sso', { body: { app_id: id, enabled } }) -} - export const fetchAppTemplates: Fetcher = ({ url }) => { return get(url) } diff --git a/web/service/base.ts b/web/service/base.ts index e3d1dc0ca2..4b08736288 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -108,8 +108,12 @@ function unicodeToChar(text: string) { }) } -function requiredWebSSOLogin() { - globalThis.location.href = `/webapp-signin?redirect_url=${globalThis.location.pathname}` +function requiredWebSSOLogin(message?: string) { + const params = new URLSearchParams() + params.append('redirect_url', globalThis.location.pathname) + if (message) + params.append('message', message) + globalThis.location.href = `/webapp-signin?${params.toString()}` } export function format(text: string) { @@ -397,6 +401,9 @@ export const ssePost = async ( }).catch(() => { res.json().then((data: any) => { if (isPublicAPI) { + if (data.code === 'web_app_access_denied') + requiredWebSSOLogin(data.message) + if (data.code === 'web_sso_auth_required') requiredWebSSOLogin() @@ -426,29 +433,29 @@ export const ssePost = async ( } onData?.(str, isFirstMessage, moreInfo) }, - onCompleted, - onThought, - onMessageEnd, - onMessageReplace, - onFile, - onWorkflowStarted, - onWorkflowFinished, - onNodeStarted, - onNodeFinished, - onIterationStart, - onIterationNext, - onIterationFinish, - onLoopStart, - onLoopNext, - onLoopFinish, - onNodeRetry, - onParallelBranchStarted, - onParallelBranchFinished, - onTextChunk, - onTTSChunk, - onTTSEnd, - onTextReplace, - onAgentLog, + onCompleted, + onThought, + onMessageEnd, + onMessageReplace, + onFile, + onWorkflowStarted, + onWorkflowFinished, + onNodeStarted, + onNodeFinished, + onIterationStart, + onIterationNext, + onIterationFinish, + onLoopStart, + onLoopNext, + onLoopFinish, + onNodeRetry, + onParallelBranchStarted, + onParallelBranchFinished, + onTextChunk, + onTTSChunk, + onTTSEnd, + onTextReplace, + onAgentLog, ) }).catch((e) => { if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property')) @@ -475,6 +482,10 @@ export const request = async(url: string, options = {}, otherOptions?: IOther // special code const { code, message } = errRespData // webapp sso + if (code === 'web_app_access_denied') { + requiredWebSSOLogin(message) + return Promise.reject(err) + } if (code === 'web_sso_auth_required') { requiredWebSSOLogin() return Promise.reject(err) diff --git a/web/service/fetch.ts b/web/service/fetch.ts index 5d09256f1d..713b34cdb9 100644 --- a/web/service/fetch.ts +++ b/web/service/fetch.ts @@ -135,9 +135,9 @@ async function base(url: string, options: FetchOptionType = {}, otherOptions: let base: string if (isMarketplaceAPI) base = MARKETPLACE_API_PREFIX - else if (isPublicAPI) + else if (isPublicAPI) base = PUBLIC_API_PREFIX - else + else base = API_PREFIX if (getAbortController) { diff --git a/web/service/share.ts b/web/service/share.ts index 7cc292eb09..7fb1562185 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -33,7 +33,7 @@ import type { ConversationItem, } from '@/models/share' import type { ChatConfig } from '@/app/components/base/chat/types' -import type { SystemFeatures } from '@/types/feature' +import type { AccessMode } from '@/models/access-control' function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) { switch (action) { @@ -186,10 +186,6 @@ export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = ' return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise } -export const fetchSystemFeatures = async () => { - return (getAction('get', false))(getUrl('system-features', false, '')) as Promise -} - export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), { params: { @@ -268,3 +264,17 @@ export const fetchAccessToken = async (appCode: string, userId?: string) => { const url = userId ? `/passport?user_id=${encodeURIComponent(userId)}` : '/passport' return get(url, { headers }) as Promise<{ access_token: string }> } + +export const getAppAccessMode = (appId: string, isInstalledApp: boolean) => { + if (isInstalledApp) + return consoleGet<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) + + return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appId=${appId}`) +} + +export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { + if (isInstalledApp) + return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) + + return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`) +} diff --git a/web/themes/manual-dark.css b/web/themes/manual-dark.css index 881f9d2c47..ed5f12bbc4 100644 --- a/web/themes/manual-dark.css +++ b/web/themes/manual-dark.css @@ -1,65 +1,66 @@ html[data-theme="dark"] { - --color-premium-yearly-tip-text-background: linear-gradient(91deg, #FDB022 2.18%, #F79009 108.79%); - --color-premium-badge-background: linear-gradient(95deg, rgba(103, 111, 131, 0.90) 0%, rgba(73, 84, 100, 0.90) 105.58%), var(--util-colors-gray-gray-200, #18222F); - --color-premium-text-background: linear-gradient(92deg, rgba(249, 250, 251, 0.95) 0%, rgba(233, 235, 240, 0.95) 97.78%); - --color-premium-badge-border-highlight-color: #ffffff33; - --color-price-enterprise-background: linear-gradient(180deg, rgba(185, 211, 234, 0.00) 0%, rgba(180, 209, 234, 0.92) 100%); - --color-grid-mask-background: linear-gradient(0deg, rgba(0, 0, 0, 0.00) 0%, rgba(24, 24, 25, 0.1) 62.25%, rgba(24, 24, 25, 0.10) 100%); - --color-chatbot-bg: linear-gradient(180deg, - rgba(34, 34, 37, 0.9) 0%, - rgba(29, 29, 32, 0.9) 90.48%); - --color-chat-bubble-bg: linear-gradient(180deg, - rgba(200, 206, 218, 0.08) 0%, - rgba(200, 206, 218, 0.02) 100%); - --color-chat-input-mask: linear-gradient(180deg, - rgba(24, 24, 27, 0.04) 0%, - rgba(24, 24, 27, 0.60) 100%); - --color-workflow-process-bg: linear-gradient(90deg, - rgba(24, 24, 27, 0.25) 0%, - rgba(24, 24, 27, 0.04) 100%); - --color-workflow-run-failed-bg: linear-gradient(98deg, - rgba(240, 68, 56, 0.12) 0%, - rgba(0, 0, 0, 0) 26.01%); - --color-workflow-batch-failed-bg: linear-gradient(92deg, - rgba(240, 68, 56, 0.3) 0%, - rgba(0, 0, 0, 0) 100%); - --color-marketplace-divider-bg: linear-gradient(90deg, - rgba(200, 206, 218, 0.14) 0%, - rgba(0, 0, 0, 0) 100%); - --color-marketplace-plugin-empty: linear-gradient(180deg, - rgba(0, 0, 0, 0) 0%, - #222225 100%); - --color-toast-success-bg: linear-gradient(92deg, - rgba(23, 178, 106, 0.3) 0%, - rgba(0, 0, 0, 0) 100%); - --color-toast-warning-bg: linear-gradient(92deg, - rgba(247, 144, 9, 0.3) 0%, - rgba(0, 0, 0, 0) 100%); - --color-toast-error-bg: linear-gradient(92deg, - rgba(240, 68, 56, 0.3) 0%, - rgba(0, 0, 0, 0) 100%); - --color-toast-info-bg: linear-gradient(92deg, - rgba(11, 165, 236, 0.3) 0%); - --color-account-teams-bg: linear-gradient(271deg, - rgba(34, 34, 37, 0.9) -0.1%, - rgba(29, 29, 32, 0.9) 98.26%); - --color-app-detail-bg: linear-gradient(169deg, - #1D1D20 1.18%, - #222225 99.52%); - --color-app-detail-overlay-bg: linear-gradient(270deg, - rgba(0, 0, 0, 0.00) 0%, - rgba(24, 24, 27, 0.02) 8%, - rgba(24, 24, 27, 0.54) 100%); - --color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%); - --color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%); - --color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%); - --color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%); - --color-dataset-option-card-blue-gradient: linear-gradient(90deg, #24252E 0%, #1E1E21 100%); - --color-dataset-option-card-purple-gradient: linear-gradient(90deg, #25242E 0%, #1E1E21 100%); - --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%); - --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%); - --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg, - rgba(24, 24, 27, 0.08) 0%, - rgba(0, 0, 0, 0) 100%); - --color-line-divider-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.14) 0%, rgba(0, 0, 0, 0) 100%); + --color-chatbot-bg: linear-gradient(180deg, + rgba(34, 34, 37, 0.9) 0%, + rgba(29, 29, 32, 0.9) 90.48%); + --color-chat-bubble-bg: linear-gradient(180deg, + rgba(200, 206, 218, 0.08) 0%, + rgba(200, 206, 218, 0.02) 100%); + --color-chat-input-mask: linear-gradient(180deg, + rgba(24, 24, 27, 0.04) 0%, + rgba(24, 24, 27, 0.60) 100%); + --color-workflow-process-bg: linear-gradient(90deg, + rgba(24, 24, 27, 0.25) 0%, + rgba(24, 24, 27, 0.04) 100%); + --color-workflow-run-failed-bg: linear-gradient(98deg, + rgba(240, 68, 56, 0.12) 0%, + rgba(0, 0, 0, 0) 26.01%); + --color-workflow-batch-failed-bg: linear-gradient(92deg, + rgba(240, 68, 56, 0.3) 0%, + rgba(0, 0, 0, 0) 100%); + --color-marketplace-divider-bg: linear-gradient(90deg, + rgba(200, 206, 218, 0.14) 0%, + rgba(0, 0, 0, 0) 100%); + --color-marketplace-plugin-empty: linear-gradient(180deg, + rgba(0, 0, 0, 0) 0%, + #222225 100%); + --color-toast-success-bg: linear-gradient(92deg, + rgba(23, 178, 106, 0.3) 0%, + rgba(0, 0, 0, 0) 100%); + --color-toast-warning-bg: linear-gradient(92deg, + rgba(247, 144, 9, 0.3) 0%, + rgba(0, 0, 0, 0) 100%); + --color-toast-error-bg: linear-gradient(92deg, + rgba(240, 68, 56, 0.3) 0%, + rgba(0, 0, 0, 0) 100%); + --color-toast-info-bg: linear-gradient(92deg, + rgba(11, 165, 236, 0.3) 0%); + --color-account-teams-bg: linear-gradient(271deg, + rgba(34, 34, 37, 0.9) -0.1%, + rgba(29, 29, 32, 0.9) 98.26%); + --color-app-detail-bg: linear-gradient(169deg, + #1D1D20 1.18%, + #222225 99.52%); + --color-app-detail-overlay-bg: linear-gradient(270deg, + rgba(0, 0, 0, 0.00) 0%, + rgba(24, 24, 27, 0.02) 8%, + rgba(24, 24, 27, 0.54) 100%); + --color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%); + --color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%); + --color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%); + --color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%); + --color-dataset-option-card-blue-gradient: linear-gradient(90deg, #24252E 0%, #1E1E21 100%); + --color-dataset-option-card-purple-gradient: linear-gradient(90deg, #25242E 0%, #1E1E21 100%); + --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%); + --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%); + --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg, + rgba(24, 24, 27, 0.08) 0%, + rgba(0, 0, 0, 0) 100%); + --color-line-divider-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.14) 0%, rgba(0, 0, 0, 0) 100%); + --color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.03) 100%); + --color-premium-yearly-tip-text-background: linear-gradient(91deg, #FDB022 2.18%, #F79009 108.79%); + --color-premium-badge-background: linear-gradient(95deg, rgba(103, 111, 131, 0.90) 0%, rgba(73, 84, 100, 0.90) 105.58%), var(--util-colors-gray-gray-200, #18222F); + --color-premium-text-background: linear-gradient(92deg, rgba(249, 250, 251, 0.95) 0%, rgba(233, 235, 240, 0.95) 97.78%); + --color-premium-badge-border-highlight-color: #ffffff33; + --color-price-enterprise-background: linear-gradient(180deg, rgba(185, 211, 234, 0.00) 0%, rgba(180, 209, 234, 0.92) 100%); + --color-grid-mask-background: linear-gradient(0deg, rgba(0, 0, 0, 0.00) 0%, rgba(24, 24, 25, 0.1) 62.25%, rgba(24, 24, 25, 0.10) 100%); } diff --git a/web/themes/manual-light.css b/web/themes/manual-light.css index ab6f14423f..c20155036e 100644 --- a/web/themes/manual-light.css +++ b/web/themes/manual-light.css @@ -1,65 +1,66 @@ html[data-theme="light"] { - --color-premium-yearly-tip-text-background: linear-gradient(91deg, #F79009 2.18%, #DC6803 108.79%); - --color-premium-badge-background: linear-gradient(95deg, rgba(152, 162, 178, 0.90) 0%, rgba(103, 111, 131, 0.90) 105.58%); - --color-premium-text-background: linear-gradient(92deg, rgba(252, 252, 253, 0.95) 0%, rgba(242, 244, 247, 0.95) 97.78%); - --color-premium-badge-border-highlight-color: #fffffff2; - --color-price-enterprise-background: linear-gradient(180deg, rgba(185, 211, 234, 0.00) 0%, rgba(180, 209, 234, 0.92) 100%); - --color-grid-mask-background: linear-gradient(0deg, #FFF 0%, rgba(217, 217, 217, 0.10) 62.25%, rgba(217, 217, 217, 0.10) 100%); - --color-chatbot-bg: linear-gradient(180deg, - rgba(249, 250, 251, 0.9) 0%, - rgba(242, 244, 247, 0.9) 90.48%); - --color-chat-bubble-bg: linear-gradient(180deg, - #fff 0%, - rgba(255, 255, 255, 0.6) 100%); - --color-chat-input-mask: linear-gradient(180deg, - rgba(255, 255, 255, 0.01) 0%, - #F2F4F7 100%); - --color-workflow-process-bg: linear-gradient(90deg, - rgba(200, 206, 218, 0.2) 0%, - rgba(200, 206, 218, 0.04) 100%); - --color-workflow-run-failed-bg: linear-gradient(98deg, - rgba(240, 68, 56, 0.10) 0%, - rgba(255, 255, 255, 0) 26.01%); - --color-workflow-batch-failed-bg: linear-gradient(92deg, - rgba(240, 68, 56, 0.25) 0%, - rgba(255, 255, 255, 0) 100%); - --color-marketplace-divider-bg: linear-gradient(90deg, - rgba(16, 24, 40, 0.08) 0%, - rgba(255, 255, 255, 0) 100%); - --color-marketplace-plugin-empty: linear-gradient(180deg, - rgba(255, 255, 255, 0) 0%, - #fcfcfd 100%); - --color-toast-success-bg: linear-gradient(92deg, - rgba(23, 178, 106, 0.25) 0%, - rgba(255, 255, 255, 0) 100%); - --color-toast-warning-bg: linear-gradient(92deg, - rgba(247, 144, 9, 0.25) 0%, - rgba(255, 255, 255, 0) 100%); - --color-toast-error-bg: linear-gradient(92deg, - rgba(240, 68, 56, 0.25) 0%, - rgba(255, 255, 255, 0) 100%); - --color-toast-info-bg: linear-gradient(92deg, - rgba(11, 165, 236, 0.25) 0%); - --color-account-teams-bg: linear-gradient(271deg, - rgba(249, 250, 251, 0.9) -0.1%, - rgba(242, 244, 247, 0.9) 98.26%); - --color-app-detail-bg: linear-gradient(169deg, - #F2F4F7 1.18%, - #F9FAFB 99.52%); - --color-app-detail-overlay-bg: linear-gradient(270deg, - rgba(0, 0, 0, 0.00) 0%, - rgba(16, 24, 40, 0.01) 8%, - rgba(16, 24, 40, 0.18) 100%); - --color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%); - --color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%); - --color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%); - --color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%); - --color-dataset-option-card-blue-gradient: linear-gradient(90deg, #F2F4F7 0%, #F9FAFB 100%); - --color-dataset-option-card-purple-gradient: linear-gradient(90deg, #F0EEFA 0%, #F9FAFB 100%); - --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%); - --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%); - --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg, - rgba(200, 206, 218, 0.2) 0%, - rgba(255, 255, 255, 0) 100%); - --color-line-divider-bg: linear-gradient(90deg, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255, 0) 100%); + --color-chatbot-bg: linear-gradient(180deg, + rgba(249, 250, 251, 0.9) 0%, + rgba(242, 244, 247, 0.9) 90.48%); + --color-chat-bubble-bg: linear-gradient(180deg, + #fff 0%, + rgba(255, 255, 255, 0.6) 100%); + --color-chat-input-mask: linear-gradient(180deg, + rgba(255, 255, 255, 0.01) 0%, + #F2F4F7 100%); + --color-workflow-process-bg: linear-gradient(90deg, + rgba(200, 206, 218, 0.2) 0%, + rgba(200, 206, 218, 0.04) 100%); + --color-workflow-run-failed-bg: linear-gradient(98deg, + rgba(240, 68, 56, 0.10) 0%, + rgba(255, 255, 255, 0) 26.01%); + --color-workflow-batch-failed-bg: linear-gradient(92deg, + rgba(240, 68, 56, 0.25) 0%, + rgba(255, 255, 255, 0) 100%); + --color-marketplace-divider-bg: linear-gradient(90deg, + rgba(16, 24, 40, 0.08) 0%, + rgba(255, 255, 255, 0) 100%); + --color-marketplace-plugin-empty: linear-gradient(180deg, + rgba(255, 255, 255, 0) 0%, + #fcfcfd 100%); + --color-toast-success-bg: linear-gradient(92deg, + rgba(23, 178, 106, 0.25) 0%, + rgba(255, 255, 255, 0) 100%); + --color-toast-warning-bg: linear-gradient(92deg, + rgba(247, 144, 9, 0.25) 0%, + rgba(255, 255, 255, 0) 100%); + --color-toast-error-bg: linear-gradient(92deg, + rgba(240, 68, 56, 0.25) 0%, + rgba(255, 255, 255, 0) 100%); + --color-toast-info-bg: linear-gradient(92deg, + rgba(11, 165, 236, 0.25) 0%); + --color-account-teams-bg: linear-gradient(271deg, + rgba(249, 250, 251, 0.9) -0.1%, + rgba(242, 244, 247, 0.9) 98.26%); + --color-app-detail-bg: linear-gradient(169deg, + #F2F4F7 1.18%, + #F9FAFB 99.52%); + --color-app-detail-overlay-bg: linear-gradient(270deg, + rgba(0, 0, 0, 0.00) 0%, + rgba(16, 24, 40, 0.01) 8%, + rgba(16, 24, 40, 0.18) 100%); + --color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%); + --color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%); + --color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%); + --color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%); + --color-dataset-option-card-blue-gradient: linear-gradient(90deg, #F2F4F7 0%, #F9FAFB 100%); + --color-dataset-option-card-purple-gradient: linear-gradient(90deg, #F0EEFA 0%, #F9FAFB 100%); + --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%); + --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%); + --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg, + rgba(200, 206, 218, 0.2) 0%, + rgba(255, 255, 255, 0) 100%); + --color-line-divider-bg: linear-gradient(90deg, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255, 0) 100%); + --color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.08) 100%); + --color-premium-yearly-tip-text-background: linear-gradient(91deg, #F79009 2.18%, #DC6803 108.79%); + --color-premium-badge-background: linear-gradient(95deg, rgba(152, 162, 178, 0.90) 0%, rgba(103, 111, 131, 0.90) 105.58%); + --color-premium-text-background: linear-gradient(92deg, rgba(252, 252, 253, 0.95) 0%, rgba(242, 244, 247, 0.95) 97.78%); + --color-premium-badge-border-highlight-color: #fffffff2; + --color-price-enterprise-background: linear-gradient(180deg, rgba(185, 211, 234, 0.00) 0%, rgba(180, 209, 234, 0.92) 100%); + --color-grid-mask-background: linear-gradient(0deg, #FFF 0%, rgba(217, 217, 217, 0.10) 62.25%, rgba(217, 217, 217, 0.10) 100%); } diff --git a/web/types/app.ts b/web/types/app.ts index 39f011dcaa..a6d2faea55 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -7,6 +7,7 @@ import type { WeightedScoreEnum, } from '@/models/datasets' import type { UploadFileSetting } from '@/app/components/workflow/types' +import type { AccessMode } from '@/models/access-control' export enum Theme { light = 'light', @@ -359,6 +360,8 @@ export type App = { updated_at: number updated_by?: string } + /** access control */ + access_mode: AccessMode } export type AppSSO = { diff --git a/web/types/feature.ts b/web/types/feature.ts index 3d7763bf46..cc945754b1 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -23,7 +23,6 @@ export type SystemFeatures = { sso_enforced_for_signin_protocol: SSOProtocol | '' sso_enforced_for_web: boolean sso_enforced_for_web_protocol: SSOProtocol | '' - enable_web_sso_switch_component: boolean enable_marketplace: boolean enable_email_code_login: boolean enable_email_password_login: boolean @@ -32,6 +31,22 @@ export type SystemFeatures = { is_allow_register: boolean is_email_setup: boolean license: License + branding: { + enabled: boolean + login_page_logo: string + workspace_logo: string + favicon: string + application_title: string + } + webapp_auth: { + enabled: boolean + allow_sso: boolean + sso_config: { + protocol: SSOProtocol | '' + } + allow_email_code_login: boolean + allow_email_password_login: boolean + } } export const defaultSystemFeatures: SystemFeatures = { @@ -39,7 +54,6 @@ export const defaultSystemFeatures: SystemFeatures = { sso_enforced_for_signin_protocol: '', sso_enforced_for_web: false, sso_enforced_for_web_protocol: '', - enable_web_sso_switch_component: false, enable_marketplace: false, enable_email_code_login: false, enable_email_password_login: false, @@ -51,4 +65,20 @@ export const defaultSystemFeatures: SystemFeatures = { status: LicenseStatus.NONE, expired_at: '', }, + branding: { + enabled: false, + login_page_logo: '', + workspace_logo: '', + favicon: '', + application_title: 'test title', + }, + webapp_auth: { + enabled: false, + allow_sso: false, + sso_config: { + protocol: '', + }, + allow_email_code_login: false, + allow_email_password_login: false, + }, }