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 (
-
-
+
+
{t('app.editApp')}
-
-
+
+
{t('app.duplicate')}
-
+
{t('app.export')}
{(app.mode === 'completion' || app.mode === 'chat') && (
<>
-
-
+
{t('app.switch')}
-
+
>
)}
-
-
+
+
{t('app.openInExplore')}
-
-
+ {
+ systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && <>
+
+ {t('app.accessControl')}
+
+
+ >
+ }
+
{t('common.operation.delete')}
-
+
)
}
@@ -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 &&
{t(`${prefixSettings}.more.copyrightTooltip`)}
+
{t(`${prefixSettings}.more.copyrightTooltip`)}
}
asChild={false}
>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
@@ -450,7 +425,6 @@ const SettingsModal: FC = ({
{t('common.operation.cancel')}
{t('common.operation.save')}
-
{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 && (
-
- )}
- {!appData?.custom_config?.replace_webapp_logo && (
-
- )}
+ {systemFeatures.branding.enabled ? (
+
+ ) : (
+
)
+ }
)}
+ {!!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 (
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 && (
-
- )}
- {!customConfig?.replace_webapp_logo && (
+ {systemFeatures.branding.enabled ? (
+
+ ) : (
)}
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 (
+
+ )
+}
+
+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,
+ },
}