Merge branch 'main' into feat/rag-pipeline

pull/21398/head
zxhlyh 11 months ago
commit 5b4d04b348

@ -119,9 +119,6 @@ class ForgotPasswordResetApi(Resource):
if not reset_data: if not reset_data:
raise InvalidTokenError() raise InvalidTokenError()
# Must use token in reset phase # 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": if reset_data.get("phase", "") != "reset":
raise InvalidTokenError() raise InvalidTokenError()

@ -59,7 +59,14 @@ class InstalledAppsListApi(Resource):
if FeatureService.get_system_features().webapp_auth.enabled: if FeatureService.get_system_features().webapp_auth.enabled:
user_id = current_user.id user_id = current_user.id
res = [] res = []
app_ids = [installed_app["app"].id for installed_app in installed_app_list]
webapp_settings = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids)
for installed_app in installed_app_list: for installed_app in installed_app_list:
webapp_setting = webapp_settings.get(installed_app["app"].id)
if not webapp_setting:
continue
if webapp_setting.access_mode == "sso_verified":
continue
app_code = AppService.get_app_code_by_id(str(installed_app["app"].id)) app_code = AppService.get_app_code_by_id(str(installed_app["app"].id))
if EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( if EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
user_id=user_id, user_id=user_id,

@ -44,6 +44,17 @@ def only_edition_cloud(view):
return decorated return decorated
def only_edition_enterprise(view):
@wraps(view)
def decorated(*args, **kwargs):
if not dify_config.ENTERPRISE_ENABLED:
abort(404)
return view(*args, **kwargs)
return decorated
def only_edition_self_hosted(view): def only_edition_self_hosted(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):

@ -15,4 +15,17 @@ api.add_resource(FileApi, "/files/upload")
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>") api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
api.add_resource(RemoteFileUploadApi, "/remote-files/upload") api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
from . import app, audio, completion, conversation, feature, message, passport, saved_message, site, workflow from . import (
app,
audio,
completion,
conversation,
feature,
forgot_password,
login,
message,
passport,
saved_message,
site,
workflow,
)

@ -10,6 +10,8 @@ from libs.passport import PassportService
from models.model import App, AppMode from models.model import App, AppMode
from services.app_service import AppService from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService
class AppParameterApi(WebApiResource): class AppParameterApi(WebApiResource):
@ -46,10 +48,22 @@ class AppMeta(WebApiResource):
class AppAccessMode(Resource): class AppAccessMode(Resource):
def get(self): def get(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("appId", type=str, required=True, location="args") parser.add_argument("appId", type=str, required=False, location="args")
parser.add_argument("appCode", type=str, required=False, location="args")
args = parser.parse_args() args = parser.parse_args()
app_id = args["appId"] features = FeatureService.get_system_features()
if not features.webapp_auth.enabled:
return {"accessMode": "public"}
app_id = args.get("appId")
if args.get("appCode"):
app_code = args["appCode"]
app_id = AppService.get_app_id_by_code(app_code)
if not app_id:
raise ValueError("appId or appCode must be provided")
res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id) res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
return {"accessMode": res.access_mode} return {"accessMode": res.access_mode}
@ -75,6 +89,10 @@ class AppWebAuthPermission(Resource):
except Exception as e: except Exception as e:
pass pass
features = FeatureService.get_system_features()
if not features.webapp_auth.enabled:
return {"result": True}
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("appId", type=str, required=True, location="args") parser.add_argument("appId", type=str, required=True, location="args")
args = parser.parse_args() args = parser.parse_args()
@ -82,7 +100,9 @@ class AppWebAuthPermission(Resource):
app_id = args["appId"] app_id = args["appId"]
app_code = AppService.get_app_code_by_id(app_id) app_code = AppService.get_app_code_by_id(app_id)
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code) res = True
if WebAppAuthService.is_app_require_permission_check(app_id=app_id):
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code)
return {"result": res} return {"result": res}

@ -0,0 +1,147 @@
import base64
import secrets
from flask import request
from flask_restful import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from controllers.console.auth.error import (
EmailCodeError,
EmailPasswordResetLimitError,
InvalidEmailError,
InvalidTokenError,
PasswordMismatchError,
)
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
from controllers.console.wraps import email_password_login_enabled, only_edition_enterprise, setup_required
from controllers.web import api
from extensions.ext_database import db
from libs.helper import email, extract_remote_ip
from libs.password import hash_password, valid_password
from models.account import Account
from services.account_service import AccountService
class ForgotPasswordSendEmailApi(Resource):
@only_edition_enterprise
@setup_required
@email_password_login_enabled
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()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
token = None
if account is None:
raise AccountNotFound()
else:
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
return {"result": "success", "data": token}
class ForgotPasswordCheckApi(Resource):
@only_edition_enterprise
@setup_required
@email_password_login_enabled
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, nullable=False, location="json")
args = parser.parse_args()
user_email = args["email"]
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(args["email"])
if is_forgot_password_error_rate_limit:
raise EmailPasswordResetLimitError()
token_data = AccountService.get_reset_password_data(args["token"])
if token_data is None:
raise InvalidTokenError()
if user_email != token_data.get("email"):
raise InvalidEmailError()
if args["code"] != token_data.get("code"):
AccountService.add_forgot_password_error_rate_limit(args["email"])
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_reset_password_token(args["token"])
# Refresh token data by generating a new token
_, new_token = AccountService.generate_reset_password_token(
user_email, code=args["code"], additional_data={"phase": "reset"}
)
AccountService.reset_forgot_password_error_rate_limit(args["email"])
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
class ForgotPasswordResetApi(Resource):
@only_edition_enterprise
@setup_required
@email_password_login_enabled
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
parser.add_argument("new_password", type=valid_password, required=True, nullable=False, location="json")
parser.add_argument("password_confirm", type=valid_password, required=True, nullable=False, location="json")
args = parser.parse_args()
# Validate passwords match
if args["new_password"] != args["password_confirm"]:
raise PasswordMismatchError()
# Validate token and get reset data
reset_data = AccountService.get_reset_password_data(args["token"])
if not reset_data:
raise InvalidTokenError()
# Must use token in reset phase
if reset_data.get("phase", "") != "reset":
raise InvalidTokenError()
# Revoke token to prevent reuse
AccountService.revoke_reset_password_token(args["token"])
# Generate secure salt and hash password
salt = secrets.token_bytes(16)
password_hashed = hash_password(args["new_password"], salt)
email = reset_data.get("email", "")
with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()
if account:
self._update_existing_account(account, password_hashed, salt, session)
else:
raise AccountNotFound()
return {"result": "success"}
def _update_existing_account(self, account, password_hashed, salt, session):
# Update existing account credentials
account.password = base64.b64encode(password_hashed).decode()
account.password_salt = base64.b64encode(salt).decode()
session.commit()
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets")

@ -1,12 +1,11 @@
from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
from jwt import InvalidTokenError # type: ignore from jwt import InvalidTokenError # type: ignore
from werkzeug.exceptions import BadRequest
import services import services
from controllers.console.auth.error import EmailCodeError, EmailOrPasswordMismatchError, InvalidEmailError from controllers.console.auth.error import EmailCodeError, EmailOrPasswordMismatchError, InvalidEmailError
from controllers.console.error import AccountBannedError, AccountNotFound from controllers.console.error import AccountBannedError, AccountNotFound
from controllers.console.wraps import setup_required from controllers.console.wraps import only_edition_enterprise, setup_required
from controllers.web import api
from libs.helper import email from libs.helper import email
from libs.password import valid_password from libs.password import valid_password
from services.account_service import AccountService from services.account_service import AccountService
@ -16,6 +15,8 @@ from services.webapp_auth_service import WebAppAuthService
class LoginApi(Resource): class LoginApi(Resource):
"""Resource for web app email/password login.""" """Resource for web app email/password login."""
@setup_required
@only_edition_enterprise
def post(self): def post(self):
"""Authenticate user and login.""" """Authenticate user and login."""
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@ -23,10 +24,6 @@ class LoginApi(Resource):
parser.add_argument("password", type=valid_password, required=True, location="json") parser.add_argument("password", type=valid_password, required=True, location="json")
args = parser.parse_args() 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: try:
account = WebAppAuthService.authenticate(args["email"], args["password"]) account = WebAppAuthService.authenticate(args["email"], args["password"])
except services.errors.account.AccountLoginError: except services.errors.account.AccountLoginError:
@ -36,12 +33,8 @@ class LoginApi(Resource):
except services.errors.account.AccountNotFoundError: except services.errors.account.AccountNotFoundError:
raise AccountNotFound() raise AccountNotFound()
WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code) token = WebAppAuthService.login(account=account)
return {"result": "success", "data": {"access_token": token}}
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): # class LogoutApi(Resource):
@ -56,6 +49,7 @@ class LoginApi(Resource):
class EmailCodeLoginSendEmailApi(Resource): class EmailCodeLoginSendEmailApi(Resource):
@setup_required @setup_required
@only_edition_enterprise
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")
@ -78,6 +72,7 @@ class EmailCodeLoginSendEmailApi(Resource):
class EmailCodeLoginApi(Resource): class EmailCodeLoginApi(Resource):
@setup_required @setup_required
@only_edition_enterprise
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json") parser.add_argument("email", type=str, required=True, location="json")
@ -86,9 +81,6 @@ class EmailCodeLoginApi(Resource):
args = parser.parse_args() args = parser.parse_args()
user_email = args["email"] 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"]) token_data = WebAppAuthService.get_email_code_login_data(args["token"])
if token_data is None: if token_data is None:
@ -105,16 +97,12 @@ class EmailCodeLoginApi(Resource):
if not account: if not account:
raise AccountNotFound() raise AccountNotFound()
WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code) token = WebAppAuthService.login(account=account)
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"]) AccountService.reset_login_error_rate_limit(args["email"])
return {"result": "success", "token": token} return {"result": "success", "data": {"access_token": token}}
# api.add_resource(LoginApi, "/login") api.add_resource(LoginApi, "/login")
# api.add_resource(LogoutApi, "/logout") # api.add_resource(LogoutApi, "/logout")
# api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login")
# api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") api.add_resource(EmailCodeLoginApi, "/email-code-login/validity")

@ -1,9 +1,11 @@
import uuid import uuid
from datetime import UTC, datetime, timedelta
from flask import request from flask import request
from flask_restful import Resource from flask_restful import Resource
from werkzeug.exceptions import NotFound, Unauthorized from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config
from controllers.web import api from controllers.web import api
from controllers.web.error import WebAppAuthRequiredError from controllers.web.error import WebAppAuthRequiredError
from extensions.ext_database import db from extensions.ext_database import db
@ -11,6 +13,7 @@ from libs.passport import PassportService
from models.model import App, EndUser, Site from models.model import App, EndUser, Site
from services.enterprise.enterprise_service import EnterpriseService from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
class PassportResource(Resource): class PassportResource(Resource):
@ -20,10 +23,19 @@ class PassportResource(Resource):
system_features = FeatureService.get_system_features() system_features = FeatureService.get_system_features()
app_code = request.headers.get("X-App-Code") app_code = request.headers.get("X-App-Code")
user_id = request.args.get("user_id") user_id = request.args.get("user_id")
web_app_access_token = request.args.get("web_app_access_token")
if app_code is None: if app_code is None:
raise Unauthorized("X-App-Code header is missing.") raise Unauthorized("X-App-Code header is missing.")
# exchange token for enterprise logined web user
enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token)
if enterprise_user_decoded:
# a web user has already logged in, exchange a token for this app without redirecting to the login page
return exchange_token_for_existing_web_user(
app_code=app_code, enterprise_user_decoded=enterprise_user_decoded
)
if system_features.webapp_auth.enabled: if system_features.webapp_auth.enabled:
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) 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": if not app_settings or not app_settings.access_mode == "public":
@ -84,6 +96,128 @@ class PassportResource(Resource):
api.add_resource(PassportResource, "/passport") api.add_resource(PassportResource, "/passport")
def decode_enterprise_webapp_user_id(jwt_token: str | None):
"""
Decode the enterprise user session from the Authorization header.
"""
if not jwt_token:
return None
decoded = PassportService().verify(jwt_token)
source = decoded.get("token_source")
if not source or source != "webapp_login_token":
raise Unauthorized("Invalid token source. Expected 'webapp_login_token'.")
return decoded
def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict):
"""
Exchange a token for an existing web user session.
"""
user_id = enterprise_user_decoded.get("user_id")
end_user_id = enterprise_user_decoded.get("end_user_id")
session_id = enterprise_user_decoded.get("session_id")
user_auth_type = enterprise_user_decoded.get("auth_type")
if not user_auth_type:
raise Unauthorized("Missing auth_type in the token.")
site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first()
if not site:
raise NotFound()
app_model = db.session.query(App).filter(App.id == site.app_id).first()
if not app_model or app_model.status != "normal" or not app_model.enable_site:
raise NotFound()
app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code)
if app_auth_type == WebAppAuthType.PUBLIC:
return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded)
elif app_auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external":
raise WebAppAuthRequiredError("Please login as external user.")
elif app_auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal":
raise WebAppAuthRequiredError("Please login as internal user.")
end_user = None
if end_user_id:
end_user = db.session.query(EndUser).filter(EndUser.id == end_user_id).first()
if session_id:
end_user = (
db.session.query(EndUser)
.filter(
EndUser.session_id == session_id,
EndUser.tenant_id == app_model.tenant_id,
EndUser.app_id == app_model.id,
)
.first()
)
if not end_user:
if not session_id:
raise NotFound("Missing session_id for existing web user.")
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="browser",
is_anonymous=True,
session_id=session_id,
)
db.session.add(end_user)
db.session.commit()
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": user_id,
"end_user_id": end_user.id,
"auth_type": user_auth_type,
"granted_at": int(datetime.now(UTC).timestamp()),
"token_source": "webapp",
"exp": exp,
}
token: str = PassportService().issue(payload)
return {
"access_token": token,
}
def _exchange_for_public_app_token(app_model, site, token_decoded):
user_id = token_decoded.get("user_id")
end_user = None
if user_id:
end_user = (
db.session.query(EndUser).filter(EndUser.app_id == app_model.id, EndUser.session_id == user_id).first()
)
if not end_user:
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="browser",
is_anonymous=True,
session_id=generate_session_id(),
)
db.session.add(end_user)
db.session.commit()
payload = {
"iss": site.app_id,
"sub": "Web API Passport",
"app_id": site.app_id,
"app_code": site.code,
"end_user_id": end_user.id,
}
tk = PassportService().issue(payload)
return {
"access_token": tk,
}
def generate_session_id(): def generate_session_id():
""" """
Generate a unique session ID. Generate a unique session ID.

@ -1,3 +1,4 @@
from datetime import UTC, datetime
from functools import wraps from functools import wraps
from flask import request from flask import request
@ -8,8 +9,9 @@ from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequire
from extensions.ext_database import db from extensions.ext_database import db
from libs.passport import PassportService from libs.passport import PassportService
from models.model import App, EndUser, Site from models.model import App, EndUser, Site
from services.enterprise.enterprise_service import EnterpriseService from services.enterprise.enterprise_service import EnterpriseService, WebAppSettings
from services.feature_service import FeatureService from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService
def validate_jwt_token(view=None): def validate_jwt_token(view=None):
@ -45,7 +47,8 @@ def decode_jwt_token():
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.") raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
decoded = PassportService().verify(tk) decoded = PassportService().verify(tk)
app_code = decoded.get("app_code") app_code = decoded.get("app_code")
app_model = db.session.query(App).filter(App.id == decoded["app_id"]).first() app_id = decoded.get("app_id")
app_model = db.session.query(App).filter(App.id == app_id).first()
site = db.session.query(Site).filter(Site.code == app_code).first() site = db.session.query(Site).filter(Site.code == app_code).first()
if not app_model: if not app_model:
raise NotFound() raise NotFound()
@ -53,23 +56,30 @@ def decode_jwt_token():
raise BadRequest("Site URL is no longer valid.") raise BadRequest("Site URL is no longer valid.")
if app_model.enable_site is False: if app_model.enable_site is False:
raise BadRequest("Site is disabled.") raise BadRequest("Site is disabled.")
end_user = db.session.query(EndUser).filter(EndUser.id == decoded["end_user_id"]).first() end_user_id = decoded.get("end_user_id")
end_user = db.session.query(EndUser).filter(EndUser.id == end_user_id).first()
if not end_user: if not end_user:
raise NotFound() raise NotFound()
# for enterprise webapp auth # for enterprise webapp auth
app_web_auth_enabled = False app_web_auth_enabled = False
webapp_settings = None
if system_features.webapp_auth.enabled: if system_features.webapp_auth.enabled:
app_web_auth_enabled = ( webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public" if not webapp_settings:
) raise NotFound("Web app settings not found.")
app_web_auth_enabled = webapp_settings.access_mode != "public"
_validate_webapp_token(decoded, app_web_auth_enabled, system_features.webapp_auth.enabled) _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) _validate_user_accessibility(
decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled, webapp_settings
)
return app_model, end_user return app_model, end_user
except Unauthorized as e: except Unauthorized as e:
if system_features.webapp_auth.enabled: if system_features.webapp_auth.enabled:
if not app_code:
raise Unauthorized("Please re-login to access the web app.")
app_web_auth_enabled = ( app_web_auth_enabled = (
EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=str(app_code)).access_mode != "public" EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=str(app_code)).access_mode != "public"
) )
@ -95,15 +105,41 @@ def _validate_webapp_token(decoded, app_web_auth_enabled: bool, system_webapp_au
raise Unauthorized("webapp token expired.") raise Unauthorized("webapp token expired.")
def _validate_user_accessibility(decoded, app_code, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool): def _validate_user_accessibility(
decoded,
app_code,
app_web_auth_enabled: bool,
system_webapp_auth_enabled: bool,
webapp_settings: WebAppSettings | None,
):
if system_webapp_auth_enabled and app_web_auth_enabled: if system_webapp_auth_enabled and app_web_auth_enabled:
# Check if the user is allowed to access the web app # Check if the user is allowed to access the web app
user_id = decoded.get("user_id") user_id = decoded.get("user_id")
if not user_id: if not user_id:
raise WebAppAuthRequiredError() raise WebAppAuthRequiredError()
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code): if not webapp_settings:
raise WebAppAuthAccessDeniedError() raise WebAppAuthRequiredError("Web app settings not found.")
if WebAppAuthService.is_app_require_permission_check(access_mode=webapp_settings.access_mode):
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code):
raise WebAppAuthAccessDeniedError()
auth_type = decoded.get("auth_type")
granted_at = decoded.get("granted_at")
if not auth_type:
raise WebAppAuthAccessDeniedError("Missing auth_type in the token.")
if not granted_at:
raise WebAppAuthAccessDeniedError("Missing granted_at in the token.")
# check if sso has been updated
if auth_type == "external":
last_update_time = EnterpriseService.get_app_sso_settings_last_update_time()
if granted_at and datetime.fromtimestamp(granted_at, tz=UTC) < last_update_time:
raise WebAppAuthAccessDeniedError("SSO settings have been updated. Please re-login.")
elif auth_type == "internal":
last_update_time = EnterpriseService.get_workspace_sso_settings_last_update_time()
if granted_at and datetime.fromtimestamp(granted_at, tz=UTC) < last_update_time:
raise WebAppAuthAccessDeniedError("SSO settings have been updated. Please re-login.")
class WebApiResource(Resource): class WebApiResource(Resource):

@ -98,6 +98,7 @@ class WeaveConfig(BaseTracingConfig):
entity: str | None = None entity: str | None = None
project: str project: str
endpoint: str = "https://trace.wandb.ai" endpoint: str = "https://trace.wandb.ai"
host: str | None = None
@field_validator("endpoint") @field_validator("endpoint")
@classmethod @classmethod
@ -109,6 +110,14 @@ class WeaveConfig(BaseTracingConfig):
return v return v
@field_validator("host")
@classmethod
def validate_host(cls, v, info: ValidationInfo):
if v is not None and v != "":
if not v.startswith(("https://", "http://")):
raise ValueError("host must start with https:// or http://")
return v
OPS_FILE_PATH = "ops_trace/" OPS_FILE_PATH = "ops_trace/"
OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE" OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE"

@ -81,7 +81,7 @@ class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]):
return { return {
"config_class": WeaveConfig, "config_class": WeaveConfig,
"secret_keys": ["api_key"], "secret_keys": ["api_key"],
"other_keys": ["project", "entity", "endpoint"], "other_keys": ["project", "entity", "endpoint", "host"],
"trace_instance": WeaveDataTrace, "trace_instance": WeaveDataTrace,
} }

@ -40,9 +40,14 @@ class WeaveDataTrace(BaseTraceInstance):
self.weave_api_key = weave_config.api_key self.weave_api_key = weave_config.api_key
self.project_name = weave_config.project self.project_name = weave_config.project
self.entity = weave_config.entity self.entity = weave_config.entity
self.host = weave_config.host
# Login with API key first, including host if provided
if self.host:
login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host)
else:
login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
# Login with API key first
login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
if not login_status: if not login_status:
logger.error("Failed to login to Weights & Biases with the provided API key") logger.error("Failed to login to Weights & Biases with the provided API key")
raise ValueError("Weave login failed") raise ValueError("Weave login failed")
@ -386,7 +391,11 @@ class WeaveDataTrace(BaseTraceInstance):
def api_check(self): def api_check(self):
try: try:
login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) if self.host:
login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host)
else:
login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
if not login_status: if not login_status:
raise ValueError("Weave login failed") raise ValueError("Weave login failed")
else: else:

@ -184,7 +184,16 @@ class OpenSearchVector(BaseVector):
} }
document_ids_filter = kwargs.get("document_ids_filter") document_ids_filter = kwargs.get("document_ids_filter")
if document_ids_filter: if document_ids_filter:
query["query"] = {"terms": {"metadata.document_id": document_ids_filter}} query["query"] = {
"script_score": {
"query": {"bool": {"filter": [{"terms": {Field.DOCUMENT_ID.value: document_ids_filter}}]}},
"script": {
"source": "knn_score",
"lang": "knn",
"params": {"field": Field.VECTOR.value, "query_value": query_vector, "space_type": "l2"},
},
}
}
try: try:
response = self._client.search(index=self._collection_name.lower(), body=query) response = self._client.search(index=self._collection_name.lower(), body=query)

@ -303,7 +303,6 @@ class OracleVector(BaseVector):
return docs return docs
else: else:
return [Document(page_content="", metadata={})] return [Document(page_content="", metadata={})]
return []
def delete(self) -> None: def delete(self) -> None:
with self._get_connection() as conn: with self._get_connection() as conn:

@ -153,8 +153,6 @@ class DatasetMultiRetrieverTool(DatasetRetrieverBaseTool):
return str("\n".join(document_context_list)) return str("\n".join(document_context_list))
return "" return ""
raise RuntimeError("not segments found")
def _retriever( def _retriever(
self, self,
flask_app: Flask, flask_app: Flask,

@ -132,3 +132,12 @@ class KnowledgeRetrievalNodeData(BaseNodeData):
metadata_model_config: Optional[ModelConfig] = None metadata_model_config: Optional[ModelConfig] = None
metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None
vision: VisionConfig = Field(default_factory=VisionConfig) vision: VisionConfig = Field(default_factory=VisionConfig)
@property
def structured_output_enabled(self) -> bool:
# NOTE(QuantumGhost): Temporary workaround for issue #20725
# (https://github.com/langgenius/dify/issues/20725).
#
# The proper fix would be to make `KnowledgeRetrievalNode` inherit
# from `BaseNode` instead of `LLMNode`.
return False

@ -19,3 +19,12 @@ class QuestionClassifierNodeData(BaseNodeData):
instruction: Optional[str] = None instruction: Optional[str] = None
memory: Optional[MemoryConfig] = None memory: Optional[MemoryConfig] = None
vision: VisionConfig = Field(default_factory=VisionConfig) vision: VisionConfig = Field(default_factory=VisionConfig)
@property
def structured_output_enabled(self) -> bool:
# NOTE(QuantumGhost): Temporary workaround for issue #20725
# (https://github.com/langgenius/dify/issues/20725).
#
# The proper fix would be to make `QuestionClassifierNode` inherit
# from `BaseNode` instead of `LLMNode`.
return False

@ -57,6 +57,9 @@ def load_user_from_request(request_from_flask_login):
raise Unauthorized("Invalid Authorization token.") raise Unauthorized("Invalid Authorization token.")
decoded = PassportService().verify(auth_token) decoded = PassportService().verify(auth_token)
user_id = decoded.get("user_id") user_id = decoded.get("user_id")
source = decoded.get("token_source")
if source:
raise Unauthorized("Invalid Authorization token.")
if not user_id: if not user_id:
raise Unauthorized("Invalid Authorization token.") raise Unauthorized("Invalid Authorization token.")

@ -0,0 +1,60 @@
"""`workflow_draft_varaibles` add `node_execution_id` column, add an index for `workflow_node_executions`.
Revision ID: 4474872b0ee6
Revises: 2adcbe1f5dfb
Create Date: 2025-06-06 14:24:44.213018
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4474872b0ee6'
down_revision = '2adcbe1f5dfb'
branch_labels = None
depends_on = None
def upgrade():
# `CREATE INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block`
# context manager to wrap the index creation statement.
# Reference:
#
# - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot.
# - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block
with op.get_context().autocommit_block():
op.create_index(
op.f('workflow_node_executions_tenant_id_idx'),
"workflow_node_executions",
['tenant_id', 'workflow_id', 'node_id', sa.literal_column('created_at DESC')],
unique=False,
postgresql_concurrently=True,
)
with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op:
batch_op.add_column(sa.Column('node_execution_id', models.types.StringUUID(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# `DROP INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block`
# context manager to wrap the index creation statement.
# Reference:
#
# - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot.
# - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block
# `DROP INDEX CONCURRENTLY` cannot run within a transaction, so commit existing transactions first.
# Reference:
#
# https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot.
with op.get_context().autocommit_block():
op.drop_index(op.f('workflow_node_executions_tenant_id_idx'), postgresql_concurrently=True)
with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op:
batch_op.drop_column('node_execution_id')
# ### end Alembic commands ###

@ -16,8 +16,8 @@ if TYPE_CHECKING:
from models.model import AppMode from models.model import AppMode
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import UniqueConstraint, func from sqlalchemy import Index, PrimaryKeyConstraint, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, declared_attr, mapped_column
from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE
from core.helper import encrypter from core.helper import encrypter
@ -590,28 +590,48 @@ class WorkflowNodeExecutionModel(Base):
""" """
__tablename__ = "workflow_node_executions" __tablename__ = "workflow_node_executions"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"), @declared_attr
db.Index( def __table_args__(cls): # noqa
"workflow_node_execution_workflow_run_idx", return (
"tenant_id", PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"),
"app_id", Index(
"workflow_id", "workflow_node_execution_workflow_run_idx",
"triggered_from", "tenant_id",
"workflow_run_id", "app_id",
), "workflow_id",
db.Index( "triggered_from",
"workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id" "workflow_run_id",
), ),
db.Index( Index(
"workflow_node_execution_id_idx", "workflow_node_execution_node_run_idx",
"tenant_id", "tenant_id",
"app_id", "app_id",
"workflow_id", "workflow_id",
"triggered_from", "triggered_from",
"node_execution_id", "node_id",
), ),
) Index(
"workflow_node_execution_id_idx",
"tenant_id",
"app_id",
"workflow_id",
"triggered_from",
"node_execution_id",
),
Index(
# The first argument is the index name,
# which we leave as `None`` to allow auto-generation by the ORM.
None,
cls.tenant_id,
cls.workflow_id,
cls.node_id,
# MyPy may flag the following line because it doesn't recognize that
# the `declared_attr` decorator passes the receiving class as the first
# argument to this method, allowing us to reference class attributes.
cls.created_at.desc(), # type: ignore
),
)
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID) tenant_id: Mapped[str] = mapped_column(StringUUID)
@ -885,14 +905,29 @@ class WorkflowDraftVariable(Base):
selector: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="selector") selector: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="selector")
# The data type of this variable's value
value_type: Mapped[SegmentType] = mapped_column(EnumText(SegmentType, length=20)) value_type: Mapped[SegmentType] = mapped_column(EnumText(SegmentType, length=20))
# JSON string
# The variable's value serialized as a JSON string
value: Mapped[str] = mapped_column(sa.Text, nullable=False, name="value") value: Mapped[str] = mapped_column(sa.Text, nullable=False, name="value")
# visible # Controls whether the variable should be displayed in the variable inspection panel
visible: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True) visible: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
# Determines whether this variable can be modified by users
editable: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False) editable: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
# The `node_execution_id` field identifies the workflow node execution that created this variable.
# It corresponds to the `id` field in the `WorkflowNodeExecutionModel` model.
#
# This field is not `None` for system variables and node variables, and is `None`
# for conversation variables.
node_execution_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
default=None,
)
def get_selector(self) -> list[str]: def get_selector(self) -> list[str]:
selector = json.loads(self.selector) selector = json.loads(self.selector)
if not isinstance(selector, list): if not isinstance(selector, list):

@ -395,3 +395,15 @@ class AppService:
if not site: if not site:
raise ValueError(f"App with id {app_id} not found") raise ValueError(f"App with id {app_id} not found")
return str(site.code) return str(site.code)
@staticmethod
def get_app_id_by_code(app_code: str) -> str:
"""
Get app id by app code
:param app_code: app code
:return: app id
"""
site = db.session.query(Site).filter(Site.code == app_code).first()
if not site:
raise ValueError(f"App with code {app_code} not found")
return str(site.app_id)

@ -1,3 +1,5 @@
from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from services.enterprise.base import EnterpriseRequest from services.enterprise.base import EnterpriseRequest
@ -5,7 +7,7 @@ from services.enterprise.base import EnterpriseRequest
class WebAppSettings(BaseModel): class WebAppSettings(BaseModel):
access_mode: str = Field( access_mode: str = Field(
description="Access mode for the web app. Can be 'public' or 'private'", description="Access mode for the web app. Can be 'public', 'private', 'private_all', 'sso_verified'",
default="private", default="private",
alias="accessMode", alias="accessMode",
) )
@ -20,6 +22,28 @@ class EnterpriseService:
def get_workspace_info(cls, tenant_id: str): def get_workspace_info(cls, tenant_id: str):
return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info") return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info")
@classmethod
def get_app_sso_settings_last_update_time(cls) -> datetime:
data = EnterpriseRequest.send_request("GET", "/sso/app/last-update-time")
if not data:
raise ValueError("No data found.")
try:
# parse the UTC timestamp from the response
return datetime.fromisoformat(data.replace("Z", "+00:00"))
except ValueError as e:
raise ValueError(f"Invalid date format: {data}") from e
@classmethod
def get_workspace_sso_settings_last_update_time(cls) -> datetime:
data = EnterpriseRequest.send_request("GET", "/sso/workspace/last-update-time")
if not data:
raise ValueError("No data found.")
try:
# parse the UTC timestamp from the response
return datetime.fromisoformat(data.replace("Z", "+00:00"))
except ValueError as e:
raise ValueError(f"Invalid date format: {data}") from e
class WebAppAuth: class WebAppAuth:
@classmethod @classmethod
def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str): def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str):

@ -1,3 +1,4 @@
import enum
import secrets import secrets
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any, Optional, cast from typing import Any, Optional, cast
@ -5,27 +6,33 @@ from typing import Any, Optional, cast
from werkzeug.exceptions import NotFound, Unauthorized from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config from configs import dify_config
from controllers.web.error import WebAppAuthAccessDeniedError
from extensions.ext_database import db from extensions.ext_database import db
from libs.helper import TokenManager from libs.helper import TokenManager
from libs.passport import PassportService from libs.passport import PassportService
from libs.password import compare_password from libs.password import compare_password
from models.account import Account, AccountStatus from models.account import Account, AccountStatus
from models.model import App, EndUser, Site from models.model import App, EndUser, Site
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService from services.enterprise.enterprise_service import EnterpriseService
from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError 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 from tasks.mail_email_code_login import send_email_code_login_mail_task
class WebAppAuthType(enum.StrEnum):
"""Enum for web app authentication types."""
PUBLIC = "public"
INTERNAL = "internal"
EXTERNAL = "external"
class WebAppAuthService: class WebAppAuthService:
"""Service for web app authentication.""" """Service for web app authentication."""
@staticmethod @staticmethod
def authenticate(email: str, password: str) -> Account: def authenticate(email: str, password: str) -> Account:
"""authenticate account with email and password""" """authenticate account with email and password"""
account = db.session.query(Account).filter_by(email=email).first()
account = Account.query.filter_by(email=email).first()
if not account: if not account:
raise AccountNotFoundError() raise AccountNotFoundError()
@ -38,12 +45,8 @@ class WebAppAuthService:
return cast(Account, account) return cast(Account, account)
@classmethod @classmethod
def login(cls, account: Account, app_code: str, end_user_id: str) -> str: def login(cls, account: Account) -> str:
site = db.session.query(Site).filter(Site.code == app_code).first() access_token = cls._get_account_jwt_token(account=account)
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 return access_token
@ -68,7 +71,7 @@ class WebAppAuthService:
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
token = TokenManager.generate_token( token = TokenManager.generate_token(
account=account, email=email, token_type="webapp_email_code_login", additional_data={"code": code} account=account, email=email, token_type="email_code_login", additional_data={"code": code}
) )
send_email_code_login_mail_task.delay( send_email_code_login_mail_task.delay(
language=language, language=language,
@ -80,11 +83,11 @@ class WebAppAuthService:
@classmethod @classmethod
def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]: def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "webapp_email_code_login") return TokenManager.get_token_data(token, "email_code_login")
@classmethod @classmethod
def revoke_email_code_login_token(cls, token: str): def revoke_email_code_login_token(cls, token: str):
TokenManager.revoke_token(token, "webapp_email_code_login") TokenManager.revoke_token(token, "email_code_login")
@classmethod @classmethod
def create_end_user(cls, app_code, email) -> EndUser: def create_end_user(cls, app_code, email) -> EndUser:
@ -109,33 +112,67 @@ class WebAppAuthService:
return end_user return end_user
@classmethod @classmethod
def _validate_user_accessibility(cls, account: Account, app_code: str): def _get_account_jwt_token(cls, account: Account) -> 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_dt = datetime.now(UTC) + timedelta(hours=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24)
exp = int(exp_dt.timestamp()) exp = int(exp_dt.timestamp())
payload = { payload = {
"iss": site.id,
"sub": "Web API Passport", "sub": "Web API Passport",
"app_id": site.app_id,
"app_code": site.code,
"user_id": account.id, "user_id": account.id,
"end_user_id": end_user_id, "session_id": account.email,
"token_source": "webapp", "token_source": "webapp_login_token",
"auth_type": "internal",
"exp": exp, "exp": exp,
} }
token: str = PassportService().issue(payload) token: str = PassportService().issue(payload)
return token return token
@classmethod
def is_app_require_permission_check(
cls, app_code: Optional[str] = None, app_id: Optional[str] = None, access_mode: Optional[str] = None
) -> bool:
"""
Check if the app requires permission check based on its access mode.
"""
modes_requiring_permission_check = [
"private",
"private_all",
]
if access_mode:
return access_mode in modes_requiring_permission_check
if not app_code and not app_id:
raise ValueError("Either app_code or app_id must be provided.")
if app_code:
app_id = AppService.get_app_id_by_code(app_code)
if not app_id:
raise ValueError("App ID could not be determined from the provided app_code.")
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
if webapp_settings and webapp_settings.access_mode in modes_requiring_permission_check:
return True
return False
@classmethod
def get_app_auth_type(cls, app_code: str | None = None, access_mode: str | None = None) -> WebAppAuthType:
"""
Get the authentication type for the app based on its access mode.
"""
if not app_code and not access_mode:
raise ValueError("Either app_code or access_mode must be provided.")
if access_mode:
if access_mode == "public":
return WebAppAuthType.PUBLIC
elif access_mode in ["private", "private_all"]:
return WebAppAuthType.INTERNAL
elif access_mode == "sso_verified":
return WebAppAuthType.EXTERNAL
if app_code:
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code)
return cls.get_app_auth_type(access_mode=webapp_settings.access_mode)
raise ValueError("Could not determine app authentication type.")

@ -47,22 +47,24 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
return ( return (
<div> <div>
<div className='mb-4 flex items-center justify-between'> <div className='mb-4'>
<div className='system-xl-semibold flex flex-row items-center text-text-primary'> <div className='system-xl-semibold mb-2 text-text-primary'>{t('common.appMenus.overview')}</div>
<span className='mr-3'>{t('appOverview.analysis.title')}</span> <div className='flex items-center justify-between'>
<SimpleSelect <div className='flex flex-row items-center'>
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} <SimpleSelect
className='mt-0 !w-40' items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
onSelect={(item) => { className='mt-0 !w-40'
const id = item.value onSelect={(item) => {
const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' const id = item.value
const name = item.name || t('appLog.filter.period.allTime') const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1'
onSelect({ value, name }) const name = item.name || t('appLog.filter.period.allTime')
}} onSelect({ value, name })
defaultValue={'2'} }}
/> defaultValue={'2'}
/>
</div>
{headerRight}
</div> </div>
{headerRight}
</div> </div>
{!isWorkflow && ( {!isWorkflow && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'> <div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>

@ -23,19 +23,6 @@ import Divider from '@/app/components/base/divider'
const I18N_PREFIX = 'app.tracing' const I18N_PREFIX = 'app.tracing'
const Title = ({
className,
}: {
className?: string
}) => {
const { t } = useTranslation()
return (
<div className={cn('system-xl-semibold flex items-center text-text-primary', className)}>
{t('common.appMenus.overview')}
</div>
)
}
const Panel: FC = () => { const Panel: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const pathname = usePathname() const pathname = usePathname()

@ -55,6 +55,7 @@ const weaveConfigTemplate = {
entity: '', entity: '',
project: '', project: '',
endpoint: '', endpoint: '',
host: '',
} }
const ProviderConfigModal: FC<Props> = ({ const ProviderConfigModal: FC<Props> = ({
@ -226,6 +227,13 @@ const ProviderConfigModal: FC<Props> = ({
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder={'https://trace.wandb.ai/'} placeholder={'https://trace.wandb.ai/'}
/> />
<Field
label='Host'
labelClassName='!text-sm'
value={(config as WeaveConfig).host}
onChange={handleConfigChange('host')}
placeholder={'https://api.wandb.ai'}
/>
</> </>
)} )}
{type === TracingProvider.langSmith && ( {type === TracingProvider.langSmith && (

@ -29,4 +29,5 @@ export type WeaveConfig = {
entity: string entity: string
project: string project: string
endpoint: string endpoint: string
host: string
} }

@ -23,10 +23,12 @@ const WebSSOForm: FC = () => {
const redirectUrl = searchParams.get('redirect_url') const redirectUrl = searchParams.get('redirect_url')
const tokenFromUrl = searchParams.get('web_sso_token') const tokenFromUrl = searchParams.get('web_sso_token')
const message = searchParams.get('message') const message = searchParams.get('message')
const code = searchParams.get('code')
const getSigninUrl = useCallback(() => { const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams) const params = new URLSearchParams(searchParams)
params.delete('message') params.delete('message')
params.delete('code')
return `/webapp-signin?${params.toString()}` return `/webapp-signin?${params.toString()}`
}, [searchParams]) }, [searchParams])
@ -85,8 +87,8 @@ const WebSSOForm: FC = () => {
if (message) { if (message) {
return <div className='flex h-full flex-col items-center justify-center gap-y-4'> return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
<AppUnavailable className='h-auto w-auto' code={t('share.common.appUnavailable')} unknownReason={message} /> <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span> <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
</div> </div>
} }
if (!redirectUrl) { if (!redirectUrl) {

@ -278,7 +278,7 @@ const AppPublisher = ({
onClick={() => { onClick={() => {
setShowAppAccessControl(true) setShowAppAccessControl(true)
}}> }}>
<div className='flex grow items-center gap-x-1.5 pr-1'> <div className='flex grow items-center gap-x-1.5 overflow-hidden pr-1'>
{appDetail?.access_mode === AccessMode.ORGANIZATION {appDetail?.access_mode === AccessMode.ORGANIZATION
&& <> && <>
<RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' />
@ -288,7 +288,9 @@ const AppPublisher = ({
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
&& <> && <>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p> <div className='grow truncate'>
<span className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</span>
</div>
</> </>
} }
{appDetail?.access_mode === AccessMode.PUBLIC {appDetail?.access_mode === AccessMode.PUBLIC

@ -21,7 +21,7 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({
return ( return (
<div className={classNames('flex h-screen w-screen items-center justify-center', className)}> <div className={classNames('flex h-screen w-screen items-center justify-center', className)}>
<h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' <h1 className='mr-5 h-[50px] shrink-0 pr-5 text-[24px] font-medium leading-[50px]'
style={{ style={{
borderRight: '1px solid rgba(0,0,0,.3)', borderRight: '1px solid rgba(0,0,0,.3)',
}}>{code}</h1> }}>{code}</h1>

@ -1,5 +1,7 @@
'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { import {
useCallback,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
@ -17,10 +19,12 @@ import ChatWrapper from './chat-wrapper'
import type { InstalledApp } from '@/models/explore' import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { checkOrSetAccessToken } from '@/app/components/share/utils' import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title' import useDocumentTitle from '@/hooks/use-document-title'
import { useTranslation } from 'react-i18next'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
type ChatWithHistoryProps = { type ChatWithHistoryProps = {
className?: string className?: string
@ -38,6 +42,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
isMobile, isMobile,
themeBuilder, themeBuilder,
sidebarCollapseState, sidebarCollapseState,
isInstalledApp,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState const isSidebarCollapsed = sidebarCollapseState
const customConfig = appData?.custom_config const customConfig = appData?.custom_config
@ -51,13 +56,34 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
useDocumentTitle(site?.title || 'Chat') useDocumentTitle(site?.title || 'Chat')
const { t } = useTranslation()
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams)
params.delete('message')
params.set('redirect_url', pathname)
return `/webapp-signin?${params.toString()}`
}, [searchParams, pathname])
const backToHome = useCallback(() => {
removeAccessToken()
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
if (appInfoLoading) { if (appInfoLoading) {
return ( return (
<Loading type='app' /> <Loading type='app' />
) )
} }
if (!userCanAccess) if (!userCanAccess) {
return <AppUnavailable code={403} unknownReason='no permission.' /> return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
{!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
</div>
}
if (appInfoError) { if (appInfoError) {
return ( return (

@ -192,7 +192,7 @@ const ChatInputArea = ({
<Textarea <Textarea
ref={ref => textareaRef.current = ref as any} ref={ref => textareaRef.current = ref as any}
className={cn( className={cn(
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-tertiary outline-none', 'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
)} )}
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''} placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
autoFocus autoFocus

@ -303,7 +303,7 @@ const Chat: FC<ChatProps> = ({
{ {
!noChatInput && ( !noChatInput && (
<ChatInputArea <ChatInputArea
botName={appData?.site.title || ''} botName={appData?.site.title || 'Bot'}
disabled={inputDisabled} disabled={inputDisabled}
showFeatureBar={showFeatureBar} showFeatureBar={showFeatureBar}
showFileUpload={showFileUpload} showFileUpload={showFileUpload}

@ -1,4 +1,6 @@
'use client'
import { import {
useCallback,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
@ -12,7 +14,7 @@ import { useEmbeddedChatbot } from './hooks'
import { isDify } from './utils' import { isDify } from './utils'
import { useThemeContext } from './theme/theme-context' import { useThemeContext } from './theme/theme-context'
import { CssTransform } from './theme/utils' import { CssTransform } from './theme/utils'
import { checkOrSetAccessToken } from '@/app/components/share/utils' import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
@ -23,6 +25,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title' import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
const Chatbot = () => { const Chatbot = () => {
const { const {
@ -36,6 +39,7 @@ const Chatbot = () => {
chatShouldReloadKey, chatShouldReloadKey,
handleNewConversation, handleNewConversation,
themeBuilder, themeBuilder,
isInstalledApp,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
@ -51,6 +55,22 @@ const Chatbot = () => {
useDocumentTitle(site?.title || 'Chat') useDocumentTitle(site?.title || 'Chat')
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams)
params.delete('message')
params.set('redirect_url', pathname)
return `/webapp-signin?${params.toString()}`
}, [searchParams, pathname])
const backToHome = useCallback(() => {
removeAccessToken()
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
if (appInfoLoading) { if (appInfoLoading) {
return ( return (
<> <>
@ -66,8 +86,12 @@ const Chatbot = () => {
) )
} }
if (!userCanAccess) if (!userCanAccess) {
return <AppUnavailable code={403} unknownReason='no permission.' /> return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
{!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
</div>
}
if (appInfoError) { if (appInfoError) {
return ( return (
@ -141,7 +165,6 @@ const EmbeddedChatbotWrapper = () => {
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,
accessMode,
userCanAccess, userCanAccess,
appParams, appParams,
appMeta, appMeta,
@ -176,7 +199,6 @@ const EmbeddedChatbotWrapper = () => {
return <EmbeddedChatbotContext.Provider value={{ return <EmbeddedChatbotContext.Provider value={{
userCanAccess, userCanAccess,
accessMode,
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,

@ -71,7 +71,7 @@ export const ThinkBlock = ({ children, ...props }: any) => {
return ( return (
<details {...(!isComplete && { open: true })} className="group"> <details {...(!isComplete && { open: true })} className="group">
<summary className="flex cursor-pointer select-none list-none items-center whitespace-nowrap pl-2 font-bold text-gray-500"> <summary className="flex cursor-pointer select-none list-none items-center whitespace-nowrap pl-2 font-bold text-text-secondary">
<div className="flex shrink-0 items-center"> <div className="flex shrink-0 items-center">
<svg <svg
className="mr-2 h-3 w-3 transition-transform duration-500 group-open:rotate-90" className="mr-2 h-3 w-3 transition-transform duration-500 group-open:rotate-90"
@ -89,7 +89,7 @@ export const ThinkBlock = ({ children, ...props }: any) => {
{isComplete ? `${t('common.chat.thought')}(${elapsedTime.toFixed(1)}s)` : `${t('common.chat.thinking')}(${elapsedTime.toFixed(1)}s)`} {isComplete ? `${t('common.chat.thought')}(${elapsedTime.toFixed(1)}s)` : `${t('common.chat.thinking')}(${elapsedTime.toFixed(1)}s)`}
</div> </div>
</summary> </summary>
<div className="ml-2 border-l border-gray-300 bg-gray-50 p-3 text-gray-500"> <div className="ml-2 border-l border-components-panel-border bg-components-panel-bg-alt p-3 text-text-secondary">
{displayContent} {displayContent}
</div> </div>
</details> </details>

@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
import MarketplaceItem from '../item/marketplace-item' import MarketplaceItem from '../item/marketplace-item'
import GithubItem from '../item/github-item' import GithubItem from '../item/github-item'
import { useFetchPluginsInMarketPlaceByIds, useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import produce from 'immer' import produce from 'immer'
import PackageItem from '../item/package-item' import PackageItem from '../item/package-item'
@ -26,7 +26,18 @@ const InstallByDSLList: FC<Props> = ({
isFromMarketPlace, isFromMarketPlace,
}) => { }) => {
// DSL has id, to get plugin info to show more info // DSL has id, to get plugin info to show more info
const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByIds(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value.marketplace_plugin_unique_identifier!)) const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value
// split org, name, version by / and :
// and remove @ and its suffix
const [orgPart, nameAndVersionPart] = dependecy.marketplace_plugin_unique_identifier!.split('@')[0].split('/')
const [name, version] = nameAndVersionPart.split(':')
return {
organization: orgPart,
plugin: name,
version,
}
}))
// has meta(org,name,version), to get id // has meta(org,name,version), to get id
const { isLoading: isFetchingDataByMeta, data: infoByMeta, error: infoByMetaError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!)) const { isLoading: isFetchingDataByMeta, data: infoByMeta, error: infoByMetaError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!))
@ -82,11 +93,11 @@ const InstallByDSLList: FC<Props> = ({
}, [allPlugins]) }, [allPlugins])
useEffect(() => { useEffect(() => {
if (!isFetchingMarketplaceDataById && infoGetById?.data.plugins) { if (!isFetchingMarketplaceDataById && infoGetById?.data.list) {
const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => { const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0]
return infoGetById.data.plugins.find(item => item.plugin_id === id)! return infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin
}) })
const payloads = sortedList const payloads = sortedList
const failedIndex: number[] = [] const failedIndex: number[] = []

@ -9,7 +9,7 @@ import {
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import TabHeader from '../../base/tab-header' import TabHeader from '../../base/tab-header'
import { checkOrSetAccessToken } from '../utils' import { checkOrSetAccessToken, removeAccessToken } from '../utils'
import MenuDropdown from './menu-dropdown' import MenuDropdown from './menu-dropdown'
import RunBatch from './run-batch' import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download' import ResDownload from './run-batch/res-download'
@ -536,14 +536,31 @@ const TextGeneration: FC<IMainProps> = ({
</div> </div>
) )
const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams)
params.delete('message')
params.set('redirect_url', pathname)
return `/webapp-signin?${params.toString()}`
}, [searchParams, pathname])
const backToHome = useCallback(() => {
removeAccessToken()
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) {
return ( return (
<div className='flex h-screen items-center'> <div className='flex h-screen items-center'>
<Loading type='app' /> <Loading type='app' />
</div>) </div>)
} }
if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) {
return <AppUnavailable code={403} unknownReason='no permission.' /> return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
{!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
</div>
}
return ( return (
<div className={cn( <div className={cn(

@ -57,22 +57,6 @@ export const setAccessToken = (sharedToken: string, token: string, user_id?: str
} }
export const removeAccessToken = () => { export const removeAccessToken = () => {
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] localStorage.removeItem('token')
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2()
try {
accessTokenJson = JSON.parse(accessToken)
if (isTokenV1(accessTokenJson))
accessTokenJson = getInitialTokenV2()
}
catch {
}
localStorage.removeItem(CONVERSATION_ID_INFO)
localStorage.removeItem('webapp_access_token') localStorage.removeItem('webapp_access_token')
delete accessTokenJson[sharedToken]
localStorage.setItem('token', JSON.stringify(accessTokenJson))
} }

@ -143,7 +143,7 @@ const Item: FC<ItemProps> = ({
ref={itemRef} ref={itemRef}
className={cn( className={cn(
(isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]', (isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]',
isHovering && ((isObj || isStructureOutput) ? 'bg-primary-50' : 'bg-state-base-hover'), isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3') 'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3')
} }
onClick={handleChosen} onClick={handleChosen}

@ -108,12 +108,13 @@ function unicodeToChar(text: string) {
}) })
} }
function requiredWebSSOLogin(message?: string) { function requiredWebSSOLogin(message?: string, code?: number) {
removeAccessToken()
const params = new URLSearchParams() const params = new URLSearchParams()
params.append('redirect_url', globalThis.location.pathname) params.append('redirect_url', globalThis.location.pathname)
if (message) if (message)
params.append('message', message) params.append('message', message)
if (code)
params.append('code', String(code))
globalThis.location.href = `/webapp-signin?${params.toString()}` globalThis.location.href = `/webapp-signin?${params.toString()}`
} }
@ -403,10 +404,12 @@ export const ssePost = async (
res.json().then((data: any) => { res.json().then((data: any) => {
if (isPublicAPI) { if (isPublicAPI) {
if (data.code === 'web_app_access_denied') if (data.code === 'web_app_access_denied')
requiredWebSSOLogin(data.message) requiredWebSSOLogin(data.message, 403)
if (data.code === 'web_sso_auth_required') if (data.code === 'web_sso_auth_required') {
removeAccessToken()
requiredWebSSOLogin() requiredWebSSOLogin()
}
if (data.code === 'unauthorized') { if (data.code === 'unauthorized') {
removeAccessToken() removeAccessToken()
@ -484,10 +487,11 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther
const { code, message } = errRespData const { code, message } = errRespData
// webapp sso // webapp sso
if (code === 'web_app_access_denied') { if (code === 'web_app_access_denied') {
requiredWebSSOLogin(message) requiredWebSSOLogin(message, 403)
return Promise.reject(err) return Promise.reject(err)
} }
if (code === 'web_sso_auth_required') { if (code === 'web_sso_auth_required') {
removeAccessToken()
requiredWebSSOLogin() requiredWebSSOLogin()
return Promise.reject(err) return Promise.reject(err)
} }

Loading…
Cancel
Save