Compare commits

...

15 Commits

Author SHA1 Message Date
GareArc
98ecc0a4eb refactor: update cleanup_webapp method to use params instead of json for DELETE request
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-12-25 22:27:40 -08:00
Yunlu Wen
780f969121 fix: fixed workflow as tool files field return empty problem (#28506)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Co-authored-by: kurokobo <kuro664@gmail.com>
original fix https://github.com/langgenius/dify/pull/27925
2025-11-21 17:35:38 +08:00
NFish
338e0f74b9 hide brand name in enterprise use (#27422)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-10-24 17:26:10 +08:00
NFish
cad6db5a1d fix: show 'Invalid email or password' error tip when web app login failed (#27034)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-10-21 11:03:43 +08:00
NFish
2e9b3b8d44 Fix/web app permission check (#26821) 2025-10-21 11:03:14 +08:00
GareArc
5a80f5158f fix: clear provider model credentials cache after updates in provider configuration 2025-10-20 20:01:55 -07:00
QuantumGhost
5a92e0feee fix(api): ensure JSON responses are properly serialized in ApiTool (#27097)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-19 18:56:25 +08:00
Yeuoly
4b5196f402 fix: ensure original response are maintained by yielding text messages in ApiTool (#23456) (#25973)
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
2025-10-19 10:20:08 +08:00
Xiyuan Chen
7b64569c8c Update email templates to improve clarity and consistency in messagin… (#26881)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-10-15 16:47:54 +08:00
Xiyuan Chen
6106207039 Fix/token exp when exchange (#26707)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-10-10 00:39:37 -07:00
Garfield Dai
3b4e9b64af delete end_user check (#26402)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
2025-09-29 16:21:29 +08:00
Garfield Dai
5073ce6e22 Fix/webapp remove code (#26436)
Co-authored-by: GareArc <chen4851@purdue.edu>
2025-09-29 15:58:41 +08:00
QuantumGhost
1277a57641 fix(api): fix internal server error caused by NULL environment_variables (#26125)
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-09-23 18:21:16 +08:00
GareArc
d3ac5b1dd8 Refactor WorkflowService to handle missing default credentials gracefully
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
2025-09-19 00:27:35 -07:00
-LAN-
39a0b89b9a Fix: enforce editor-only access to chat message logs (#25936)
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
2025-09-18 22:01:56 +08:00
35 changed files with 564 additions and 151 deletions

View File

@@ -62,6 +62,9 @@ class ChatMessageListApi(Resource):
@account_initialization_required @account_initialization_required
@marshal_with(message_infinite_scroll_pagination_fields) @marshal_with(message_infinite_scroll_pagination_fields)
def get(self, app_model): def get(self, app_model):
if not isinstance(current_user, Account) or not current_user.has_edit_permission:
raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("conversation_id", required=True, type=uuid_value, location="args") parser.add_argument("conversation_id", required=True, type=uuid_value, location="args")
parser.add_argument("first_id", type=uuid_value, location="args") parser.add_argument("first_id", type=uuid_value, location="args")

View File

@@ -15,7 +15,6 @@ from libs.datetime_utils import naive_utc_now
from libs.login import current_user, login_required from libs.login import current_user, login_required
from models import Account, App, InstalledApp, RecommendedApp from models import Account, App, InstalledApp, RecommendedApp
from services.account_service import TenantService from services.account_service import TenantService
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.feature_service import FeatureService
@@ -68,31 +67,26 @@ class InstalledAppsListApi(Resource):
# Pre-filter out apps without setting or with sso_verified # Pre-filter out apps without setting or with sso_verified
filtered_installed_apps = [] filtered_installed_apps = []
app_id_to_app_code = {}
for installed_app in installed_app_list: for installed_app in installed_app_list:
app_id = installed_app["app"].id app_id = installed_app["app"].id
webapp_setting = webapp_settings.get(app_id) webapp_setting = webapp_settings.get(app_id)
if not webapp_setting or webapp_setting.access_mode == "sso_verified": if not webapp_setting or webapp_setting.access_mode == "sso_verified":
continue continue
app_code = AppService.get_app_code_by_id(str(app_id))
app_id_to_app_code[app_id] = app_code
filtered_installed_apps.append(installed_app) filtered_installed_apps.append(installed_app)
app_codes = list(app_id_to_app_code.values())
# Batch permission check # Batch permission check
app_ids = [installed_app["app"].id for installed_app in filtered_installed_apps]
permissions = EnterpriseService.WebAppAuth.batch_is_user_allowed_to_access_webapps( permissions = EnterpriseService.WebAppAuth.batch_is_user_allowed_to_access_webapps(
user_id=user_id, user_id=user_id,
app_codes=app_codes, app_ids=app_ids,
) )
# Keep only allowed apps # Keep only allowed apps
res = [] res = []
for installed_app in filtered_installed_apps: for installed_app in filtered_installed_apps:
app_id = installed_app["app"].id app_id = installed_app["app"].id
app_code = app_id_to_app_code[app_id] if permissions.get(app_id):
if permissions.get(app_code):
res.append(installed_app) res.append(installed_app)
installed_app_list = res installed_app_list = res

View File

@@ -11,7 +11,6 @@ from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db from extensions.ext_database import db
from libs.login import login_required from libs.login import login_required
from models import InstalledApp from models import InstalledApp
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.feature_service import FeatureService
@@ -57,10 +56,9 @@ def user_allowed_to_access_app(view: Callable[Concatenate[InstalledApp, P], R] |
feature = FeatureService.get_system_features() feature = FeatureService.get_system_features()
if feature.webapp_auth.enabled: if feature.webapp_auth.enabled:
app_id = installed_app.app_id 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( res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
user_id=str(current_user.id), user_id=str(current_user.id),
app_code=app_code, app_id=app_id,
) )
if not res: if not res:
raise AppAccessDeniedError() raise AppAccessDeniedError()

View File

@@ -30,7 +30,6 @@ from extensions.ext_database import db
from fields.document_fields import document_fields, document_status_fields from fields.document_fields import document_fields, document_status_fields
from libs.login import current_user from libs.login import current_user
from models.dataset import Dataset, Document, DocumentSegment from models.dataset import Dataset, Document, DocumentSegment
from models.model import EndUser
from services.dataset_service import DatasetService, DocumentService from services.dataset_service import DatasetService, DocumentService
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
from services.file_service import FileService from services.file_service import FileService
@@ -299,9 +298,6 @@ class DocumentAddByFileApi(DatasetApiResource):
if not file.filename: if not file.filename:
raise FilenameNotExistsError raise FilenameNotExistsError
if not isinstance(current_user, EndUser):
raise ValueError("Invalid user account")
upload_file = FileService.upload_file( upload_file = FileService.upload_file(
filename=file.filename, filename=file.filename,
content=file.read(), content=file.read(),
@@ -391,8 +387,6 @@ class DocumentUpdateByFileApi(DatasetApiResource):
raise FilenameNotExistsError raise FilenameNotExistsError
try: try:
if not isinstance(current_user, EndUser):
raise ValueError("Invalid user account")
upload_file = FileService.upload_file( upload_file = FileService.upload_file(
filename=file.filename, filename=file.filename,
content=file.read(), content=file.read(),

View File

@@ -160,9 +160,8 @@ class AppWebAuthPermission(Resource):
args = parser.parse_args() args = parser.parse_args()
app_id = args["appId"] app_id = args["appId"]
app_code = AppService.get_app_code_by_id(app_id)
res = True res = True
if WebAppAuthService.is_app_require_permission_check(app_id=app_id): 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) res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_id)
return {"result": res} return {"result": res}

View File

@@ -12,6 +12,7 @@ from controllers.web.error import WebAppAuthRequiredError
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.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.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
@@ -38,7 +39,7 @@ class PassportResource(Resource):
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.")
app_id = AppService.get_app_id_by_code(app_code)
# exchange token for enterprise logined web user # exchange token for enterprise logined web user
enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token) enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token)
if enterprise_user_decoded: if enterprise_user_decoded:
@@ -48,7 +49,7 @@ class PassportResource(Resource):
) )
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_id(app_id=app_id)
if not app_settings or not app_settings.access_mode == "public": if not app_settings or not app_settings.access_mode == "public":
raise WebAppAuthRequiredError() raise WebAppAuthRequiredError()
@@ -126,6 +127,8 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
end_user_id = enterprise_user_decoded.get("end_user_id") end_user_id = enterprise_user_decoded.get("end_user_id")
session_id = enterprise_user_decoded.get("session_id") session_id = enterprise_user_decoded.get("session_id")
user_auth_type = enterprise_user_decoded.get("auth_type") user_auth_type = enterprise_user_decoded.get("auth_type")
exchanged_token_expires_unix = enterprise_user_decoded.get("exp")
if not user_auth_type: if not user_auth_type:
raise Unauthorized("Missing auth_type in the token.") raise Unauthorized("Missing auth_type in the token.")
@@ -169,8 +172,11 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
) )
db.session.add(end_user) db.session.add(end_user)
db.session.commit() db.session.commit()
exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)
exp = int(exp_dt.timestamp()) exp = int((datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)).timestamp())
if exchanged_token_expires_unix:
exp = int(exchanged_token_expires_unix)
payload = { payload = {
"iss": site.id, "iss": site.id,
"sub": "Web API Passport", "sub": "Web API Passport",

View File

@@ -13,6 +13,7 @@ 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.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService, WebAppSettings 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 from services.webapp_auth_service import WebAppAuthService
@@ -37,7 +38,11 @@ def validate_jwt_token(view: Callable[Concatenate[App, EndUser, P], R] | None =
def decode_jwt_token(): def decode_jwt_token():
system_features = FeatureService.get_system_features() system_features = FeatureService.get_system_features()
app_code = str(request.headers.get("X-App-Code")) app_code = request.headers.get("X-App-Code")
if not app_code:
app_code = None
else:
app_code = str(app_code)
try: try:
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
if auth_header is None: if auth_header is None:
@@ -51,15 +56,30 @@ def decode_jwt_token():
if auth_scheme != "bearer": if auth_scheme != "bearer":
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.") raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
# Check for invalid token values
if tk in ["undefined", "null", "None", ""]:
raise Unauthorized("Invalid token provided.")
decoded = PassportService().verify(tk) decoded = PassportService().verify(tk)
app_code = decoded.get("app_code") # Preserve app_code from header if JWT token doesn't contain one
jwt_app_code = decoded.get("app_code")
if jwt_app_code:
app_code = jwt_app_code
app_id = decoded.get("app_id") app_id = decoded.get("app_id")
# Validate required fields from JWT token
if not app_id:
raise Unauthorized("Invalid token: missing app_id.")
if not app_code:
raise Unauthorized("Invalid token: missing app_code.")
with Session(db.engine, expire_on_commit=False) as session: with Session(db.engine, expire_on_commit=False) as session:
app_model = session.scalar(select(App).where(App.id == app_id)) app_model = session.scalar(select(App).where(App.id == app_id))
site = session.scalar(select(Site).where(Site.code == app_code)) site = session.scalar(select(Site).where(Site.code == app_code))
if not app_model: if not app_model:
raise NotFound() raise NotFound()
if not app_code or not site: if not site:
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.")
@@ -72,7 +92,12 @@ def decode_jwt_token():
app_web_auth_enabled = False app_web_auth_enabled = False
webapp_settings = None webapp_settings = None
if system_features.webapp_auth.enabled: if system_features.webapp_auth.enabled:
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) if not app_code:
raise BadRequest("App code is required for webapp authentication.")
if app_code in ["undefined", "null", "None", ""]:
raise BadRequest("Invalid app code provided.")
app_id = AppService.get_app_id_by_code(app_code)
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
if not webapp_settings: if not webapp_settings:
raise NotFound("Web app settings not found.") raise NotFound("Web app settings not found.")
app_web_auth_enabled = webapp_settings.access_mode != "public" app_web_auth_enabled = webapp_settings.access_mode != "public"
@@ -87,8 +112,11 @@ def decode_jwt_token():
if system_features.webapp_auth.enabled: if system_features.webapp_auth.enabled:
if not app_code: if not app_code:
raise Unauthorized("Please re-login to access the web app.") raise Unauthorized("Please re-login to access the web app.")
if app_code in ["undefined", "null", "None", ""]:
raise Unauthorized("Invalid app code provided.")
app_id = AppService.get_app_id_by_code(app_code)
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_id(app_id=app_id).access_mode != "public"
) )
if app_web_auth_enabled: if app_web_auth_enabled:
raise WebAppAuthRequiredError() raise WebAppAuthRequiredError()
@@ -129,7 +157,10 @@ def _validate_user_accessibility(
raise WebAppAuthRequiredError("Web app settings not found.") raise WebAppAuthRequiredError("Web app settings not found.")
if WebAppAuthService.is_app_require_permission_check(access_mode=webapp_settings.access_mode): 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): if not app_code or app_code in ["undefined", "null", "None", ""]:
raise WebAppAuthAccessDeniedError("Invalid app code for permission check.")
app_id = AppService.get_app_id_by_code(app_code)
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_id):
raise WebAppAuthAccessDeniedError() raise WebAppAuthAccessDeniedError()
auth_type = decoded.get("auth_type") auth_type = decoded.get("auth_type")

View File

@@ -1140,6 +1140,15 @@ class ProviderConfiguration(BaseModel):
raise ValueError("Can't add same credential") raise ValueError("Can't add same credential")
provider_model_record.credential_id = credential_record.id provider_model_record.credential_id = credential_record.id
provider_model_record.updated_at = naive_utc_now() provider_model_record.updated_at = naive_utc_now()
# clear cache
provider_model_credentials_cache = ProviderCredentialsCache(
tenant_id=self.tenant_id,
identity_id=provider_model_record.id,
cache_type=ProviderCredentialsCacheType.MODEL,
)
provider_model_credentials_cache.delete()
session.add(provider_model_record) session.add(provider_model_record)
session.commit() session.commit()
@@ -1173,6 +1182,14 @@ class ProviderConfiguration(BaseModel):
session.add(provider_model_record) session.add(provider_model_record)
session.commit() session.commit()
# clear cache
provider_model_credentials_cache = ProviderCredentialsCache(
tenant_id=self.tenant_id,
identity_id=provider_model_record.id,
cache_type=ProviderCredentialsCacheType.MODEL,
)
provider_model_credentials_cache.delete()
def delete_custom_model(self, model_type: ModelType, model: str): def delete_custom_model(self, model_type: ModelType, model: str):
""" """
Delete custom model. Delete custom model.

View File

@@ -394,8 +394,14 @@ class ApiTool(Tool):
parsed_response = self.validate_and_parse_response(response) parsed_response = self.validate_and_parse_response(response)
# assemble invoke message based on response type # assemble invoke message based on response type
if parsed_response.is_json and isinstance(parsed_response.content, dict): if parsed_response.is_json:
yield self.create_json_message(parsed_response.content) if isinstance(parsed_response.content, dict):
yield self.create_json_message(parsed_response.content)
# The yield below must be preserved to keep backward compatibility.
#
# ref: https://github.com/langgenius/dify/pull/23456#issuecomment-3182413088
yield self.create_text_message(response.text)
else: else:
# Convert to string if needed and create text message # Convert to string if needed and create text message
text_response = ( text_response = (

View File

@@ -318,7 +318,13 @@ class ToolNode(BaseNode):
json.append(message.message.json_object) json.append(message.message.json_object)
elif message.type == ToolInvokeMessage.MessageType.LINK: elif message.type == ToolInvokeMessage.MessageType.LINK:
assert isinstance(message.message, ToolInvokeMessage.TextMessage) assert isinstance(message.message, ToolInvokeMessage.TextMessage)
stream_text = f"Link: {message.message.text}\n" # Check if this LINK message is a file link
file_obj = (message.meta or {}).get("file")
if isinstance(file_obj, File):
files.append(file_obj)
stream_text = f"File: {message.message.text}\n"
else:
stream_text = f"Link: {message.message.text}\n"
text += stream_text text += stream_text
yield RunStreamChunkEvent(chunk_content=stream_text, from_variable_selector=[node_id, "text"]) yield RunStreamChunkEvent(chunk_content=stream_text, from_variable_selector=[node_id, "text"])
elif message.type == ToolInvokeMessage.MessageType.VARIABLE: elif message.type == ToolInvokeMessage.MessageType.VARIABLE:

View File

@@ -354,7 +354,7 @@ class Workflow(Base):
if not tenant_id: if not tenant_id:
return [] return []
environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables) environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables or "{}")
results = [ results = [
variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values() variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values()
] ]

View File

@@ -46,17 +46,17 @@ class EnterpriseService:
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_id: str):
params = {"userId": user_id, "appCode": app_code} params = {"userId": user_id, "appId": app_id}
data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params) data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params)
return data.get("result", False) return data.get("result", False)
@classmethod @classmethod
def batch_is_user_allowed_to_access_webapps(cls, user_id: str, app_codes: list[str]): def batch_is_user_allowed_to_access_webapps(cls, user_id: str, app_ids: list[str]):
if not app_codes: if not app_ids:
return {} return {}
body = {"userId": user_id, "appCodes": app_codes} body = {"userId": user_id, "appIds": app_ids}
data = EnterpriseRequest.send_request("POST", "/webapp/permission/batch", json=body) data = EnterpriseRequest.send_request("POST", "/webapp/permission/batch", json=body)
if not data: if not data:
raise ValueError("No data found.") raise ValueError("No data found.")
@@ -92,16 +92,6 @@ class EnterpriseService:
return ret 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 @classmethod
def update_app_access_mode(cls, app_id: str, access_mode: str): def update_app_access_mode(cls, app_id: str, access_mode: str):
if not app_id: if not app_id:
@@ -114,11 +104,11 @@ class EnterpriseService:
response = EnterpriseRequest.send_request("POST", "/webapp/access-mode", json=data) response = EnterpriseRequest.send_request("POST", "/webapp/access-mode", json=data)
return response.get("result", False) return response.get("result", False)
@classmethod @classmethod
def cleanup_webapp(cls, app_id: str): def cleanup_webapp(cls, app_id: str):
if not app_id: if not app_id:
raise ValueError("app_id must be provided.") raise ValueError("app_id must be provided.")
body = {"appId": app_id} params = {"appId": app_id}
EnterpriseRequest.send_request("DELETE", "/webapp/clean", json=body) EnterpriseRequest.send_request("DELETE", "/webapp/clean", params=params)

View File

@@ -172,7 +172,8 @@ class WebAppAuthService:
return WebAppAuthType.EXTERNAL return WebAppAuthType.EXTERNAL
if app_code: if app_code:
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code) app_id = AppService.get_app_id_by_code(app_code)
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
return cls.get_app_auth_type(access_mode=webapp_settings.access_mode) return cls.get_app_auth_type(access_mode=webapp_settings.access_mode)
raise ValueError("Could not determine app authentication type.") raise ValueError("Could not determine app authentication type.")

View File

@@ -452,7 +452,8 @@ class WorkflowService:
) )
if not default_provider: if not default_provider:
raise ValueError("No default credential found") # plugin does not require credentials, skip
return
# Check credential policy compliance using the default credential ID # Check credential policy compliance using the default credential ID
from core.helper.credential_utils import check_credential_policy_compliance from core.helper.credential_utils import check_credential_policy_compliance

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -96,7 +98,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
</style> </style>
@@ -107,7 +110,7 @@
<div class="header"></div> <div class="header"></div>
<p class="title">Confirm Your New Email Address</p> <p class="title">Confirm Your New Email Address</p>
<div class="description"> <div class="description">
<p class="content1">Youre updating the email address linked to your Dify account.</p> <p class="content1">You're updating the email address linked to your account.</p>
<p class="content2">To confirm this action, please use the verification code below.</p> <p class="content2">To confirm this action, please use the verification code below.</p>
<p class="content3">This code will only be valid for the next 5 minutes:</p> <p class="content3">This code will only be valid for the next 5 minutes:</p>
</div> </div>
@@ -118,5 +121,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -96,7 +98,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
</style> </style>
@@ -107,7 +110,7 @@
<div class="header"></div> <div class="header"></div>
<p class="title">确认您的邮箱地址变更</p> <p class="title">确认您的邮箱地址变更</p>
<div class="description"> <div class="description">
<p class="content1">您正在更新与您的 Dify 账户关联的邮箱地址。</p> <p class="content1">您正在更新与您的账户关联的邮箱地址。</p>
<p class="content2">为了确认此操作,请使用以下验证码。</p> <p class="content2">为了确认此操作,请使用以下验证码。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效</p> <p class="content3">此验证码仅在接下来的5分钟内有效</p>
</div> </div>
@@ -118,5 +121,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -96,7 +98,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
</style> </style>
@@ -107,7 +110,7 @@
<div class="header"></div> <div class="header"></div>
<p class="title">Verify Your Request to Change Email</p> <p class="title">Verify Your Request to Change Email</p>
<div class="description"> <div class="description">
<p class="content1">We received a request to change the email address associated with your Dify account.</p> <p class="content1">We received a request to change the email address associated with your account.</p>
<p class="content2">To confirm this action, please use the verification code below.</p> <p class="content2">To confirm this action, please use the verification code below.</p>
<p class="content3">This code will only be valid for the next 5 minutes:</p> <p class="content3">This code will only be valid for the next 5 minutes:</p>
</div> </div>
@@ -118,5 +121,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -96,7 +98,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
</style> </style>
@@ -107,7 +110,7 @@
<div class="header"></div> <div class="header"></div>
<p class="title">验证您的邮箱变更请求</p> <p class="title">验证您的邮箱变更请求</p>
<div class="description"> <div class="description">
<p class="content1">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p> <p class="content1">我们收到了一个变更您账户关联邮箱地址的请求。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效</p> <p class="content3">此验证码仅在接下来的5分钟内有效</p>
</div> </div>
<div class="code-content"> <div class="code-content">
@@ -117,5 +120,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<style> <style>
body { body {
@@ -10,6 +11,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.container { .container {
width: 504px; width: 504px;
min-height: 444px; min-height: 444px;
@@ -30,6 +32,7 @@
max-width: 63px; max-width: 63px;
height: auto; height: auto;
} }
.button { .button {
display: block; display: block;
padding: 8px 12px; padding: 8px 12px;
@@ -45,49 +48,56 @@
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
} }
.button:hover { .button:hover {
background-color: #004AEB; background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08); border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05); box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
} }
.content { .content {
color: #354052; color: #354052;
font-family: Inter; font-family: Inter;
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
.content1 { .content1 {
margin: 0; margin: 0;
padding-top: 24px; padding-top: 24px;
padding-bottom: 12px; padding-bottom: 12px;
font-weight: 500; font-weight: 500;
} }
.content2 { .content2 {
margin: 0; margin: 0;
padding-bottom: 12px; padding-bottom: 12px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header"></div>
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
</div>
<div class="content"> <div class="content">
<p class="content1">Dear {{ to }},</p> <p class="content1">Dear {{ to }},</p>
<p class="content2">{{ 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.</p> <p class="content2">{{ 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.</p>
<p class="content2">Click the button below to log in to {{application_title}} and join the workspace.</p> <p class="content2">Click the button below to log in to {{application_title}} and join the workspace.</p>
<p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p> <p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none"
class="button" href="{{ url }}">Login Here</a></p>
<p class="content2">Best regards,</p> <p class="content2">Best regards,</p>
<p class="content2">{{application_title}} Team</p> <p class="content2">{{application_title}} Team</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -80,10 +82,9 @@
<div class="description"> <div class="description">
<p class="content1">You have been assigned as the new owner of the workspace "{{WorkspaceName}}".</p> <p class="content1">You have been assigned as the new owner of the workspace "{{WorkspaceName}}".</p>
<p class="content2">As the new owner, you now have full administrative privileges for this workspace.</p> <p class="content2">As the new owner, you now have full administrative privileges for this workspace.</p>
<p class="content3">If you have any questions, please contact support@dify.ai.</p> <p class="content3">If you have any questions, please contact support.</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -80,10 +82,9 @@
<div class="description"> <div class="description">
<p class="content1">您已被分配为工作空间“{{WorkspaceName}}”的新所有者。</p> <p class="content1">您已被分配为工作空间“{{WorkspaceName}}”的新所有者。</p>
<p class="content2">作为新所有者,您现在对该工作空间拥有完全的管理权限。</p> <p class="content2">作为新所有者,您现在对该工作空间拥有完全的管理权限。</p>
<p class="content3">如果您有任何问题,请联系support@dify.ai</p> <p class="content3">如果您有任何问题,请联系支持团队</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -97,7 +99,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
</style> </style>
@@ -108,12 +111,14 @@
<div class="header"></div> <div class="header"></div>
<p class="title">Workspace ownership has been transferred</p> <p class="title">Workspace ownership has been transferred</p>
<div class="description"> <div class="description">
<p class="content1">You have successfully transferred ownership of the workspace "{{WorkspaceName}}" to {{NewOwnerEmail}}.</p> <p class="content1">You have successfully transferred ownership of the workspace "{{WorkspaceName}}" to
<p class="content2">You no longer have owner privileges for this workspace. Your access level has been changed to Admin.</p> {{NewOwnerEmail}}.</p>
<p class="content3">If you did not initiate this transfer or have concerns about this change, please contact support@dify.ai immediately.</p> <p class="content2">You no longer have owner privileges for this workspace. Your access level has been changed to
Admin.</p>
<p class="content3">If you did not initiate this transfer or have concerns about this change, please contact
support immediately.</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -42,7 +42,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 120%; /* 28.8px */ line-height: 120%;
/* 28.8px */
} }
.description { .description {
@@ -51,7 +52,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
@@ -97,7 +99,8 @@
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px; /* 142.857% */ line-height: 20px;
/* 142.857% */
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
</style> </style>
@@ -110,10 +113,9 @@
<div class="description"> <div class="description">
<p class="content1">您已成功将工作空间“{{WorkspaceName}}”的所有权转移给{{NewOwnerEmail}}。</p> <p class="content1">您已成功将工作空间“{{WorkspaceName}}”的所有权转移给{{NewOwnerEmail}}。</p>
<p class="content2">您不再拥有此工作空间的拥有者权限。您的访问级别已更改为管理员。</p> <p class="content2">您不再拥有此工作空间的拥有者权限。您的访问级别已更改为管理员。</p>
<p class="content3">如果您没有发起此转移或对此变更有任何疑问,请立即联系support@dify.ai</p> <p class="content3">如果您没有发起此转移或对此变更有任何疑问,请立即联系支持团队</p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,12 +1,14 @@
"""Integration tests for ChatMessageApi permission verification.""" """Integration tests for ChatMessageApi permission verification."""
import uuid import uuid
from types import SimpleNamespace
from unittest import mock from unittest import mock
import pytest import pytest
from flask.testing import FlaskClient from flask.testing import FlaskClient
from controllers.console.app import completion as completion_api from controllers.console.app import completion as completion_api
from controllers.console.app import message as message_api
from controllers.console.app import wraps from controllers.console.app import wraps
from libs.datetime_utils import naive_utc_now from libs.datetime_utils import naive_utc_now
from models import Account, App, Tenant from models import Account, App, Tenant
@@ -99,3 +101,106 @@ class TestChatMessageApiPermissions:
) )
assert response.status_code == status assert response.status_code == status
@pytest.mark.parametrize(
("role", "status"),
[
(TenantAccountRole.OWNER, 200),
(TenantAccountRole.ADMIN, 200),
(TenantAccountRole.EDITOR, 200),
(TenantAccountRole.NORMAL, 403),
(TenantAccountRole.DATASET_OPERATOR, 403),
],
)
def test_get_requires_edit_permission(
self,
test_client: FlaskClient,
auth_header,
monkeypatch,
mock_app_model,
mock_account,
role: TenantAccountRole,
status: int,
):
"""Ensure GET chat-messages endpoint enforces edit permissions."""
mock_load_app_model = mock.Mock(return_value=mock_app_model)
monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model)
conversation_id = uuid.uuid4()
created_at = naive_utc_now()
mock_conversation = SimpleNamespace(id=str(conversation_id), app_id=str(mock_app_model.id))
mock_message = SimpleNamespace(
id=str(uuid.uuid4()),
conversation_id=str(conversation_id),
inputs=[],
query="hello",
message=[{"text": "hello"}],
message_tokens=0,
re_sign_file_url_answer="",
answer_tokens=0,
provider_response_latency=0.0,
from_source="console",
from_end_user_id=None,
from_account_id=mock_account.id,
feedbacks=[],
workflow_run_id=None,
annotation=None,
annotation_hit_history=None,
created_at=created_at,
agent_thoughts=[],
message_files=[],
message_metadata_dict={},
status="success",
error="",
parent_message_id=None,
)
class MockQuery:
def __init__(self, model):
self.model = model
def where(self, *args, **kwargs):
return self
def first(self):
if getattr(self.model, "__name__", "") == "Conversation":
return mock_conversation
return None
def order_by(self, *args, **kwargs):
return self
def limit(self, *_):
return self
def all(self):
if getattr(self.model, "__name__", "") == "Message":
return [mock_message]
return []
mock_session = mock.Mock()
mock_session.query.side_effect = MockQuery
mock_session.scalar.return_value = False
monkeypatch.setattr(message_api, "db", SimpleNamespace(session=mock_session))
monkeypatch.setattr(message_api, "current_user", mock_account)
class DummyPagination:
def __init__(self, data, limit, has_more):
self.data = data
self.limit = limit
self.has_more = has_more
monkeypatch.setattr(message_api, "InfiniteScrollPagination", DummyPagination)
mock_account.role = role
response = test_client.get(
f"/console/api/apps/{mock_app_model.id}/chat-messages",
headers=auth_header,
query_string={"conversation_id": str(conversation_id)},
)
assert response.status_code == status

View File

@@ -35,9 +35,7 @@ class TestWebAppAuthService:
mock_enterprise_service.WebAppAuth.get_app_access_mode_by_id.return_value = type( mock_enterprise_service.WebAppAuth.get_app_access_mode_by_id.return_value = type(
"MockWebAppAuth", (), {"access_mode": "private"} "MockWebAppAuth", (), {"access_mode": "private"}
)() )()
mock_enterprise_service.WebAppAuth.get_app_access_mode_by_code.return_value = type( # Note: get_app_access_mode_by_code method was removed in refactoring
"MockWebAppAuth", (), {"access_mode": "private"}
)()
yield { yield {
"passport_service": mock_passport_service, "passport_service": mock_passport_service,
@@ -866,7 +864,7 @@ class TestWebAppAuthService:
mock_webapp_auth = type("MockWebAppAuth", (), {"access_mode": "sso_verified"})() mock_webapp_auth = type("MockWebAppAuth", (), {"access_mode": "sso_verified"})()
mock_external_service_dependencies[ mock_external_service_dependencies[
"enterprise_service" "enterprise_service"
].WebAppAuth.get_app_access_mode_by_code.return_value = mock_webapp_auth ].WebAppAuth.get_app_access_mode_by_id.return_value = mock_webapp_auth
# Act: Execute authentication type determination # Act: Execute authentication type determination
result = WebAppAuthService.get_app_auth_type(app_code="mock_app_code") result = WebAppAuthService.get_app_auth_type(app_code="mock_app_code")
@@ -877,7 +875,7 @@ class TestWebAppAuthService:
# Verify mock service was called correctly # Verify mock service was called correctly
mock_external_service_dependencies[ mock_external_service_dependencies[
"enterprise_service" "enterprise_service"
].WebAppAuth.get_app_access_mode_by_code.assert_called_once_with("mock_app_code") ].WebAppAuth.get_app_access_mode_by_id.assert_called_once_with("mock_app_id")
def test_get_app_auth_type_no_parameters(self, db_session_with_containers, mock_external_service_dependencies): def test_get_app_auth_type_no_parameters(self, db_session_with_containers, mock_external_service_dependencies):
""" """

View File

@@ -0,0 +1,249 @@
import json
import operator
from typing import TypeVar
from unittest.mock import Mock, patch
import httpx
import pytest
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.custom_tool.tool import ApiTool
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_bundle import ApiToolBundle
from core.tools.entities.tool_entities import (
ToolEntity,
ToolIdentity,
ToolInvokeMessage,
)
_T = TypeVar("_T")
def _get_message_by_type(msgs: list[ToolInvokeMessage], msg_type: type[_T]) -> ToolInvokeMessage | None:
return next((i for i in msgs if isinstance(i.message, msg_type)), None)
class TestApiToolInvoke:
"""Test suite for ApiTool._invoke method to ensure JSON responses are properly serialized."""
def setup_method(self):
"""Setup test fixtures."""
# Create a mock tool entity
self.mock_tool_identity = ToolIdentity(
author="test",
name="test_api_tool",
label=I18nObject(en_US="Test API Tool", zh_Hans="测试API工具"),
provider="test_provider",
)
self.mock_tool_entity = ToolEntity(identity=self.mock_tool_identity)
# Create a mock API bundle
self.mock_api_bundle = ApiToolBundle(
server_url="https://api.example.com/test",
method="GET",
openapi={},
operation_id="test_operation",
parameters=[],
author="test_author",
)
# Create a mock runtime
self.mock_runtime = Mock(spec=ToolRuntime)
self.mock_runtime.credentials = {"auth_type": "none"}
# Create the ApiTool instance
self.api_tool = ApiTool(
entity=self.mock_tool_entity,
api_bundle=self.mock_api_bundle,
runtime=self.mock_runtime,
provider_id="test_provider",
)
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_json_response_creates_text_message_with_serialized_json(self, mock_get: Mock) -> None:
"""Test that when upstream returns JSON, the output Text message contains JSON-serialized string."""
# Setup mock response with JSON content
json_response_data = {
"key": "value",
"number": 123,
"nested": {"inner": "data"},
}
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = json.dumps(json_response_data).encode("utf-8")
mock_response.json.return_value = json_response_data
mock_response.text = json.dumps(json_response_data)
mock_response.headers = {"content-type": "application/json"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 2
# Verify _invoke yields text message
text_message = _get_message_by_type(result, ToolInvokeMessage.TextMessage)
assert text_message is not None, "_invoke should yield a text message"
assert isinstance(text_message, ToolInvokeMessage)
assert text_message.type == ToolInvokeMessage.MessageType.TEXT
assert text_message.message is not None
# Verify the text contains the JSON-serialized string
# Check if message is a TextMessage
assert isinstance(text_message.message, ToolInvokeMessage.TextMessage)
# Verify it's a valid JSON string and equals to the mock response
parsed_back = json.loads(text_message.message.text)
assert parsed_back == json_response_data
# Verify _invoke yields json message
json_message = _get_message_by_type(result, ToolInvokeMessage.JsonMessage)
assert json_message is not None, "_invoke should yield a JSON message"
assert isinstance(json_message, ToolInvokeMessage)
assert json_message.type == ToolInvokeMessage.MessageType.JSON
assert json_message.message is not None
assert isinstance(json_message.message, ToolInvokeMessage.JsonMessage)
assert json_message.message.json_object == json_response_data
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
@pytest.mark.parametrize(
"test_case",
[
(
"array",
[
{"id": 1, "name": "Item 1", "active": True},
{"id": 2, "name": "Item 2", "active": False},
{"id": 3, "name": "项目 3", "active": True},
],
),
(
"string",
"string",
),
(
"number",
123.456,
),
(
"boolean",
True,
),
(
"null",
None,
),
],
ids=operator.itemgetter(0),
)
def test_invoke_with_non_dict_json_response_creates_text_message_with_serialized_json(
self, mock_get: Mock, test_case
) -> None:
"""Test that when upstream returns a non-dict JSON, the output Text message contains JSON-serialized string."""
# Setup mock response with non-dict JSON content
_, json_value = test_case
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = json.dumps(json_value).encode("utf-8")
mock_response.json.return_value = json_value
mock_response.text = json.dumps(json_value)
mock_response.headers = {"content-type": "application/json"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 1
# Verify _invoke yields a text message
text_message = _get_message_by_type(result, ToolInvokeMessage.TextMessage)
assert text_message is not None, "_invoke should yield a text message containing the serialized JSON."
assert isinstance(text_message, ToolInvokeMessage)
assert text_message.type == ToolInvokeMessage.MessageType.TEXT
assert text_message.message is not None
# Verify the text contains the JSON-serialized string
# Check if message is a TextMessage
assert isinstance(text_message.message, ToolInvokeMessage.TextMessage)
# Verify it's a valid JSON string
parsed_back = json.loads(text_message.message.text)
assert parsed_back == json_value
# Verify _invoke yields json message
json_message = _get_message_by_type(result, ToolInvokeMessage.JsonMessage)
assert json_message is None, "_invoke should not yield a JSON message for JSON array response"
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_text_response_creates_text_message_with_original_text(self, mock_get: Mock) -> None:
"""Test that when upstream returns plain text, the output Text message contains the original text."""
# Setup mock response with plain text content
text_response_data = "This is a plain text response"
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = text_response_data.encode("utf-8")
mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "doc", 0)
mock_response.text = text_response_data
mock_response.headers = {"content-type": "text/plain"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 1
# Verify it's a text message with the original text
message = result[0]
assert isinstance(message, ToolInvokeMessage)
assert message.type == ToolInvokeMessage.MessageType.TEXT
assert message.message is not None
# Check if message is a TextMessage
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
assert message.message.text == text_response_data
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_empty_response(self, mock_get: Mock) -> None:
"""Test that empty responses are handled correctly."""
# Setup mock response with empty content
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = b""
mock_response.headers = {"content-type": "application/json"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 1
# Verify it's a text message with the empty response message
message = result[0]
assert isinstance(message, ToolInvokeMessage)
assert message.type == ToolInvokeMessage.MessageType.TEXT
assert message.message is not None
# Check if message is a TextMessage
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
assert "Empty response from the tool" in message.message.text
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_error_response(self, mock_get: Mock) -> None:
"""Test that error responses are handled correctly."""
# Setup mock response with error status code
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 404
mock_response.text = "Not Found"
mock_get.return_value = mock_response
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Invoke the tool and expect an error
with pytest.raises(Exception) as exc_info:
list(result_generator) # Consume the generator to trigger the error
# Verify the error message
assert "Request failed with status code 404" in str(exc_info.value)

View File

@@ -100,7 +100,10 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
}) })
} }
} }
catch (e: any) {
if (e.code === 'authentication_failed')
Toast.notify({ type: 'error', message: e.message })
}
finally { finally {
setIsLoading(false) setIsLoading(false)
} }

View File

@@ -17,12 +17,9 @@ import type {
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
export type EmbeddedChatbotContextValue = { export type EmbeddedChatbotContextValue = {
userCanAccess?: boolean appMeta: AppMeta | null
appInfoError?: any appData: AppData | null
appInfoLoading?: boolean appParams: ChatConfig | null
appMeta?: AppMeta
appData?: AppData
appParams?: ChatConfig
appChatListDataLoading?: boolean appChatListDataLoading?: boolean
currentConversationId: string currentConversationId: string
currentConversationItem?: ConversationItem currentConversationItem?: ConversationItem
@@ -59,7 +56,10 @@ export type EmbeddedChatbotContextValue = {
} }
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
userCanAccess: false, appData: null,
appMeta: null,
appParams: null,
appChatListDataLoading: false,
currentConversationId: '', currentConversationId: '',
appPrevChatList: [], appPrevChatList: [],
pinnedConversationList: [], pinnedConversationList: [],

View File

@@ -18,9 +18,6 @@ import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils' import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils' import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import { import {
fetchAppInfo,
fetchAppMeta,
fetchAppParams,
fetchChatList, fetchChatList,
fetchConversations, fetchConversations,
generationConversationName, generationConversationName,
@@ -36,8 +33,7 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { useGetUserCanAccessApp } from '@/service/access-control' import { useWebAppStore } from '@/context/web-app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
function getFormattedChatList(messages: any[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = []
@@ -67,18 +63,10 @@ function getFormattedChatList(messages: any[]) {
export const useEmbeddedChatbot = () => { export const useEmbeddedChatbot = () => {
const isInstalledApp = false const isInstalledApp = false
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const appInfo = useWebAppStore(s => s.appInfo)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) const appMeta = useWebAppStore(s => s.appMeta)
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ const appParams = useWebAppStore(s => s.appParams)
appId: appInfo?.app_id, const appId = useMemo(() => appInfo?.app_id, [appInfo])
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const appData = useMemo(() => {
return appInfo
}, [appInfo])
const appId = useMemo(() => appData?.app_id, [appData])
const [userId, setUserId] = useState<string>() const [userId, setUserId] = useState<string>()
const [conversationId, setConversationId] = useState<string>() const [conversationId, setConversationId] = useState<string>()
@@ -145,8 +133,6 @@ export const useEmbeddedChatbot = () => {
return currentConversationId return currentConversationId
}, [currentConversationId, newConversationId]) }, [currentConversationId, newConversationId])
const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId))
const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId))
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
@@ -395,16 +381,13 @@ export const useEmbeddedChatbot = () => {
}, [isInstalledApp, appId, t, notify]) }, [isInstalledApp, appId, t, notify])
return { return {
appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp, isInstalledApp,
allowResetChat, allowResetChat,
appId, appId,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
handleConversationIdInfoChange, handleConversationIdInfoChange,
appData, appData: appInfo,
appParams: appParams || {} as ChatConfig, appParams: appParams || {} as ChatConfig,
appMeta, appMeta,
appPinnedConversationData, appPinnedConversationData,

View File

@@ -101,7 +101,6 @@ const EmbeddedChatbotWrapper = () => {
const { const {
appData, appData,
userCanAccess,
appParams, appParams,
appMeta, appMeta,
appChatListDataLoading, appChatListDataLoading,
@@ -135,7 +134,6 @@ const EmbeddedChatbotWrapper = () => {
} = useEmbeddedChatbot() } = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{ return <EmbeddedChatbotContext.Provider value={{
userCanAccess,
appData, appData,
appParams, appParams,
appMeta, appMeta,

View File

@@ -135,8 +135,8 @@ const NormalForm = () => {
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>} {!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>}
</div> </div>
: <div className="mx-auto w-full"> : <div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2> <h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('login.pageTitleForE') : t('login.pageTitle')}</h2>
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>} <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>
</div>} </div>}
<div className="relative"> <div className="relative">
<div className="mt-6 flex flex-col gap-3"> <div className="mt-6 flex flex-col gap-3">

View File

@@ -1,5 +1,6 @@
const translation = { const translation = {
pageTitle: 'Log in to Dify', pageTitle: 'Log in to Dify',
pageTitleForE: 'Hey, let\'s get started!',
welcome: '👋 Welcome! Please log in to get started.', welcome: '👋 Welcome! Please log in to get started.',
email: 'Email address', email: 'Email address',
emailPlaceholder: 'Your email', emailPlaceholder: 'Your email',

View File

@@ -1,5 +1,6 @@
const translation = { const translation = {
pageTitle: 'Dify にログイン', pageTitle: 'Dify にログイン',
pageTitleForE: 'はじめましょう!',
welcome: '👋 ようこそ!まずはログインしてご利用ください。', welcome: '👋 ようこそ!まずはログインしてご利用ください。',
email: 'メールアドレス', email: 'メールアドレス',
emailPlaceholder: 'メールアドレスを入力してください', emailPlaceholder: 'メールアドレスを入力してください',

View File

@@ -1,5 +1,6 @@
const translation = { const translation = {
pageTitle: '登录 Dify', pageTitle: '登录 Dify',
pageTitleForE: '嗨,近来可好',
welcome: '👋 欢迎!请登录以开始使用。', welcome: '👋 欢迎!请登录以开始使用。',
email: '邮箱', email: '邮箱',
emailPlaceholder: '输入邮箱地址', emailPlaceholder: '输入邮箱地址',

View File

@@ -1,5 +1,6 @@
const translation = { const translation = {
pageTitle: '嗨,近來可好', pageTitle: '嗨,近來可好',
pageTitleForE: '嗨,近來可好',
welcome: '👋 歡迎來到 Dify, 登入以繼續', welcome: '👋 歡迎來到 Dify, 登入以繼續',
email: '郵箱', email: '郵箱',
emailPlaceholder: '輸入郵箱地址', emailPlaceholder: '輸入郵箱地址',