Feat/change user email (#22213)

Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
This commit is contained in:
zyssyz123
2025-07-17 10:55:59 +08:00
committed by GitHub
parent a324d3942e
commit a4f421028c
52 changed files with 4726 additions and 327 deletions

View File

@@ -495,6 +495,8 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
# Reset password token expiry minutes
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
CREATE_TIDB_SERVICE_JOB_ENABLED=false

View File

@@ -31,6 +31,15 @@ class SecurityConfig(BaseSettings):
description="Duration in minutes for which a password reset token remains valid",
default=5,
)
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="Duration in minutes for which a change email token remains valid",
default=5,
)
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="Duration in minutes for which a owner transfer token remains valid",
default=5,
)
LOGIN_DISABLED: bool = Field(
description="Whether to disable login checks",
@@ -614,6 +623,16 @@ class AuthConfig(BaseSettings):
default=86400,
)
CHANGE_EMAIL_LOCKOUT_DURATION: PositiveInt = Field(
description="Time (in seconds) a user must wait before retrying change email after exceeding the rate limit.",
default=86400,
)
OWNER_TRANSFER_LOCKOUT_DURATION: PositiveInt = Field(
description="Time (in seconds) a user must wait before retrying owner transfer after exceeding the rate limit.",
default=86400,
)
class ModerationConfig(BaseSettings):
"""

View File

@@ -31,6 +31,18 @@ class PasswordResetRateLimitExceededError(BaseHTTPException):
code = 429
class EmailChangeRateLimitExceededError(BaseHTTPException):
error_code = "email_change_rate_limit_exceeded"
description = "Too many email change emails have been sent. Please try again in 1 minutes."
code = 429
class OwnerTransferRateLimitExceededError(BaseHTTPException):
error_code = "owner_transfer_rate_limit_exceeded"
description = "Too many owner tansfer emails have been sent. Please try again in 1 minutes."
code = 429
class EmailCodeError(BaseHTTPException):
error_code = "email_code_error"
description = "Email code is invalid or expired."
@@ -65,3 +77,39 @@ class EmailPasswordResetLimitError(BaseHTTPException):
error_code = "email_password_reset_limit"
description = "Too many failed password reset attempts. Please try again in 24 hours."
code = 429
class EmailChangeLimitError(BaseHTTPException):
error_code = "email_change_limit"
description = "Too many failed email change attempts. Please try again in 24 hours."
code = 429
class EmailAlreadyInUseError(BaseHTTPException):
error_code = "email_already_in_use"
description = "A user with this email already exists."
code = 400
class OwnerTransferLimitError(BaseHTTPException):
error_code = "owner_transfer_limit"
description = "Too many failed owner transfer attempts. Please try again in 24 hours."
code = 429
class NotOwnerError(BaseHTTPException):
error_code = "not_owner"
description = "You are not the owner of the workspace."
code = 400
class CannotTransferOwnerToSelfError(BaseHTTPException):
error_code = "cannot_transfer_owner_to_self"
description = "You cannot transfer ownership to yourself."
code = 400
class MemberNotInTenantError(BaseHTTPException):
error_code = "member_not_in_tenant"
description = "The member is not in the workspace."
code = 400

View File

@@ -4,10 +4,20 @@ import pytz
from flask import request
from flask_login import current_user
from flask_restful import Resource, fields, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
from constants.languages import supported_language
from controllers.console import api
from controllers.console.auth.error import (
EmailAlreadyInUseError,
EmailChangeLimitError,
EmailCodeError,
InvalidEmailError,
InvalidTokenError,
)
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
from controllers.console.workspace.error import (
AccountAlreadyInitedError,
CurrentPasswordIncorrectError,
@@ -18,15 +28,17 @@ from controllers.console.workspace.error import (
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_enabled,
enable_change_email,
enterprise_license_required,
only_edition_cloud,
setup_required,
)
from extensions.ext_database import db
from fields.member_fields import account_fields
from libs.helper import TimestampField, timezone
from libs.helper import TimestampField, email, extract_remote_ip, timezone
from libs.login import login_required
from models import AccountIntegrate, InvitationCode
from models.account import Account
from services.account_service import AccountService
from services.billing_service import BillingService
from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError
@@ -369,6 +381,134 @@ class EducationAutoCompleteApi(Resource):
return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"])
class ChangeEmailSendEmailApi(Resource):
@enable_change_email
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("language", type=str, required=False, location="json")
parser.add_argument("phase", type=str, required=False, location="json")
parser.add_argument("token", 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"
account = None
user_email = args["email"]
if args["phase"] is not None and args["phase"] == "new_email":
if args["token"] is None:
raise InvalidTokenError()
reset_data = AccountService.get_change_email_data(args["token"])
if reset_data is None:
raise InvalidTokenError()
user_email = reset_data.get("email", "")
if user_email != current_user.email:
raise InvalidEmailError()
else:
with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
if account is None:
raise AccountNotFound()
token = AccountService.send_change_email_email(
account=account, email=args["email"], old_email=user_email, language=language, phase=args["phase"]
)
return {"result": "success", "data": token}
class ChangeEmailCheckApi(Resource):
@enable_change_email
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, 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_change_email_error_rate_limit = AccountService.is_change_email_error_rate_limit(args["email"])
if is_change_email_error_rate_limit:
raise EmailChangeLimitError()
token_data = AccountService.get_change_email_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_change_email_error_rate_limit(args["email"])
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_change_email_token(args["token"])
# Refresh token data by generating a new token
_, new_token = AccountService.generate_change_email_token(
user_email, code=args["code"], old_email=token_data.get("old_email"), additional_data={}
)
AccountService.reset_change_email_error_rate_limit(args["email"])
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
class ChangeEmailResetApi(Resource):
@enable_change_email
@setup_required
@login_required
@account_initialization_required
@marshal_with(account_fields)
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("new_email", type=email, required=True, location="json")
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
reset_data = AccountService.get_change_email_data(args["token"])
if not reset_data:
raise InvalidTokenError()
AccountService.revoke_change_email_token(args["token"])
if not AccountService.check_email_unique(args["new_email"]):
raise EmailAlreadyInUseError()
old_email = reset_data.get("old_email", "")
if current_user.email != old_email:
raise AccountNotFound()
updated_account = AccountService.update_account(current_user, email=args["new_email"])
return updated_account
class CheckEmailUnique(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
args = parser.parse_args()
if not AccountService.check_email_unique(args["email"]):
raise EmailAlreadyInUseError()
return {"result": "success"}
# Register API resources
api.add_resource(AccountInitApi, "/account/init")
api.add_resource(AccountProfileApi, "/account/profile")
@@ -385,5 +525,10 @@ api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback")
api.add_resource(EducationVerifyApi, "/account/education/verify")
api.add_resource(EducationApi, "/account/education")
api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete")
# Change email
api.add_resource(ChangeEmailSendEmailApi, "/account/change-email")
api.add_resource(ChangeEmailCheckApi, "/account/change-email/validity")
api.add_resource(ChangeEmailResetApi, "/account/change-email/reset")
api.add_resource(CheckEmailUnique, "/account/change-email/check-email-unique")
# api.add_resource(AccountEmailApi, '/account/email')
# api.add_resource(AccountEmailVerifyApi, '/account/email-verify')

View File

@@ -1,22 +1,34 @@
from urllib import parse
from flask import request
from flask_login import current_user
from flask_restful import Resource, abort, marshal_with, reqparse
import services
from configs import dify_config
from controllers.console import api
from controllers.console.error import WorkspaceMembersLimitExceeded
from controllers.console.auth.error import (
CannotTransferOwnerToSelfError,
EmailCodeError,
InvalidEmailError,
InvalidTokenError,
MemberNotInTenantError,
NotOwnerError,
OwnerTransferLimitError,
)
from controllers.console.error import EmailSendIpLimitError, WorkspaceMembersLimitExceeded
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
is_allow_transfer_owner,
setup_required,
)
from extensions.ext_database import db
from fields.member_fields import account_with_role_list_fields
from libs.helper import extract_remote_ip
from libs.login import login_required
from models.account import Account, TenantAccountRole
from services.account_service import RegisterService, TenantService
from services.account_service import AccountService, RegisterService, TenantService
from services.errors.account import AccountAlreadyInTenantError
from services.feature_service import FeatureService
@@ -156,8 +168,148 @@ class DatasetOperatorMemberListApi(Resource):
return {"result": "success", "accounts": members}, 200
class SendOwnerTransferEmailApi(Resource):
"""Send owner transfer email."""
@setup_required
@login_required
@account_initialization_required
@is_allow_transfer_owner
def post(self):
parser = reqparse.RequestParser()
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()
# check if the current user is the owner of the workspace
if not TenantService.is_owner(current_user, current_user.current_tenant):
raise NotOwnerError()
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
email = current_user.email
token = AccountService.send_owner_transfer_email(
account=current_user,
email=email,
language=language,
workspace_name=current_user.current_tenant.name,
)
return {"result": "success", "data": token}
class OwnerTransferCheckApi(Resource):
@setup_required
@login_required
@account_initialization_required
@is_allow_transfer_owner
def post(self):
parser = reqparse.RequestParser()
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()
# check if the current user is the owner of the workspace
if not TenantService.is_owner(current_user, current_user.current_tenant):
raise NotOwnerError()
user_email = current_user.email
is_owner_transfer_error_rate_limit = AccountService.is_owner_transfer_error_rate_limit(user_email)
if is_owner_transfer_error_rate_limit:
raise OwnerTransferLimitError()
token_data = AccountService.get_owner_transfer_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_owner_transfer_error_rate_limit(user_email)
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_owner_transfer_token(args["token"])
# Refresh token data by generating a new token
_, new_token = AccountService.generate_owner_transfer_token(user_email, code=args["code"], additional_data={})
AccountService.reset_owner_transfer_error_rate_limit(user_email)
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
class OwnerTransfer(Resource):
@setup_required
@login_required
@account_initialization_required
@is_allow_transfer_owner
def post(self, member_id):
parser = reqparse.RequestParser()
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
# check if the current user is the owner of the workspace
if not TenantService.is_owner(current_user, current_user.current_tenant):
raise NotOwnerError()
if current_user.id == str(member_id):
raise CannotTransferOwnerToSelfError()
transfer_token_data = AccountService.get_owner_transfer_data(args["token"])
if not transfer_token_data:
print(transfer_token_data, "transfer_token_data")
raise InvalidTokenError()
if transfer_token_data.get("email") != current_user.email:
print(transfer_token_data.get("email"), current_user.email)
raise InvalidEmailError()
AccountService.revoke_owner_transfer_token(args["token"])
member = db.session.get(Account, str(member_id))
if not member:
abort(404)
else:
member_account = member
if not TenantService.is_member(member_account, current_user.current_tenant):
raise MemberNotInTenantError()
try:
assert member is not None, "Member not found"
TenantService.update_member_role(current_user.current_tenant, member, "owner", current_user)
AccountService.send_new_owner_transfer_notify_email(
account=member,
email=member.email,
workspace_name=current_user.current_tenant.name,
)
AccountService.send_old_owner_transfer_notify_email(
account=current_user,
email=current_user.email,
workspace_name=current_user.current_tenant.name,
new_owner_email=member.email,
)
except Exception as e:
raise ValueError(str(e))
return {"result": "success"}
api.add_resource(MemberListApi, "/workspaces/current/members")
api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email")
api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>")
api.add_resource(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role")
api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators")
# owner transfer
api.add_resource(SendOwnerTransferEmailApi, "/workspaces/current/members/send-owner-transfer-confirm-email")
api.add_resource(OwnerTransferCheckApi, "/workspaces/current/members/owner-transfer-check")
api.add_resource(OwnerTransfer, "/workspaces/current/members/<uuid:member_id>/owner-transfer")

View File

@@ -235,3 +235,29 @@ def email_password_login_enabled(view):
abort(403)
return decorated
def enable_change_email(view):
@wraps(view)
def decorated(*args, **kwargs):
features = FeatureService.get_system_features()
if features.enable_change_email:
return view(*args, **kwargs)
# otherwise, return 403
abort(403)
return decorated
def is_allow_transfer_owner(view):
@wraps(view)
def decorated(*args, **kwargs):
features = FeatureService.get_features(current_user.current_tenant_id)
if features.is_allow_transfer_workspace:
return view(*args, **kwargs)
# otherwise, return 403
abort(403)
return decorated

View File

@@ -52,8 +52,14 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError, Workspaces
from services.feature_service import FeatureService
from tasks.delete_account_task import delete_account_task
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
from tasks.mail_change_mail_task import send_change_mail_task
from tasks.mail_email_code_login import send_email_code_login_mail_task
from tasks.mail_invite_member_task import send_invite_member_mail_task
from tasks.mail_owner_transfer_task import (
send_new_owner_transfer_notify_email_task,
send_old_owner_transfer_notify_email_task,
send_owner_transfer_confirm_task,
)
from tasks.mail_reset_password_task import send_reset_password_mail_task
@@ -75,8 +81,13 @@ class AccountService:
email_code_account_deletion_rate_limiter = RateLimiter(
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
)
change_email_rate_limiter = RateLimiter(prefix="change_email_rate_limit", max_attempts=1, time_window=60 * 1)
owner_transfer_rate_limiter = RateLimiter(prefix="owner_transfer_rate_limit", max_attempts=1, time_window=60 * 1)
LOGIN_MAX_ERROR_LIMITS = 5
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
CHANGE_EMAIL_MAX_ERROR_LIMITS = 5
OWNER_TRANSFER_MAX_ERROR_LIMITS = 5
@staticmethod
def _get_refresh_token_key(refresh_token: str) -> str:
@@ -419,6 +430,101 @@ class AccountService:
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
return token
@classmethod
def send_change_email_email(
cls,
account: Optional[Account] = None,
email: Optional[str] = None,
old_email: Optional[str] = None,
language: Optional[str] = "en-US",
phase: Optional[str] = None,
):
account_email = account.email if account else email
if account_email is None:
raise ValueError("Email must be provided.")
if cls.change_email_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import EmailChangeRateLimitExceededError
raise EmailChangeRateLimitExceededError()
code, token = cls.generate_change_email_token(account_email, account, old_email=old_email)
send_change_mail_task.delay(
language=language,
to=account_email,
code=code,
phase=phase,
)
cls.change_email_rate_limiter.increment_rate_limit(account_email)
return token
@classmethod
def send_owner_transfer_email(
cls,
account: Optional[Account] = None,
email: Optional[str] = None,
language: Optional[str] = "en-US",
workspace_name: Optional[str] = "",
):
account_email = account.email if account else email
if account_email is None:
raise ValueError("Email must be provided.")
if cls.owner_transfer_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import OwnerTransferRateLimitExceededError
raise OwnerTransferRateLimitExceededError()
code, token = cls.generate_owner_transfer_token(account_email, account)
send_owner_transfer_confirm_task.delay(
language=language,
to=account_email,
code=code,
workspace=workspace_name,
)
cls.owner_transfer_rate_limiter.increment_rate_limit(account_email)
return token
@classmethod
def send_old_owner_transfer_notify_email(
cls,
account: Optional[Account] = None,
email: Optional[str] = None,
language: Optional[str] = "en-US",
workspace_name: Optional[str] = "",
new_owner_email: Optional[str] = "",
):
account_email = account.email if account else email
if account_email is None:
raise ValueError("Email must be provided.")
send_old_owner_transfer_notify_email_task.delay(
language=language,
to=account_email,
workspace=workspace_name,
new_owner_email=new_owner_email,
)
@classmethod
def send_new_owner_transfer_notify_email(
cls,
account: Optional[Account] = None,
email: Optional[str] = None,
language: Optional[str] = "en-US",
workspace_name: Optional[str] = "",
):
account_email = account.email if account else email
if account_email is None:
raise ValueError("Email must be provided.")
send_new_owner_transfer_notify_email_task.delay(
language=language,
to=account_email,
workspace=workspace_name,
)
@classmethod
def generate_reset_password_token(
cls,
@@ -435,14 +541,64 @@ class AccountService:
)
return code, token
@classmethod
def generate_change_email_token(
cls,
email: str,
account: Optional[Account] = None,
code: Optional[str] = None,
old_email: Optional[str] = None,
additional_data: dict[str, Any] = {},
):
if not code:
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
additional_data["code"] = code
additional_data["old_email"] = old_email
token = TokenManager.generate_token(
account=account, email=email, token_type="change_email", additional_data=additional_data
)
return code, token
@classmethod
def generate_owner_transfer_token(
cls,
email: str,
account: Optional[Account] = None,
code: Optional[str] = None,
additional_data: dict[str, Any] = {},
):
if not code:
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
additional_data["code"] = code
token = TokenManager.generate_token(
account=account, email=email, token_type="owner_transfer", additional_data=additional_data
)
return code, token
@classmethod
def revoke_reset_password_token(cls, token: str):
TokenManager.revoke_token(token, "reset_password")
@classmethod
def revoke_change_email_token(cls, token: str):
TokenManager.revoke_token(token, "change_email")
@classmethod
def revoke_owner_transfer_token(cls, token: str):
TokenManager.revoke_token(token, "owner_transfer")
@classmethod
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "reset_password")
@classmethod
def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "change_email")
@classmethod
def get_owner_transfer_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "owner_transfer")
@classmethod
def send_email_code_login_email(
cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US"
@@ -552,6 +708,62 @@ class AccountService:
key = f"forgot_password_error_rate_limit:{email}"
redis_client.delete(key)
@staticmethod
@redis_fallback(default_return=None)
def add_change_email_error_rate_limit(email: str) -> None:
key = f"change_email_error_rate_limit:{email}"
count = redis_client.get(key)
if count is None:
count = 0
count = int(count) + 1
redis_client.setex(key, dify_config.CHANGE_EMAIL_LOCKOUT_DURATION, count)
@staticmethod
@redis_fallback(default_return=False)
def is_change_email_error_rate_limit(email: str) -> bool:
key = f"change_email_error_rate_limit:{email}"
count = redis_client.get(key)
if count is None:
return False
count = int(count)
if count > AccountService.CHANGE_EMAIL_MAX_ERROR_LIMITS:
return True
return False
@staticmethod
@redis_fallback(default_return=None)
def reset_change_email_error_rate_limit(email: str):
key = f"change_email_error_rate_limit:{email}"
redis_client.delete(key)
@staticmethod
@redis_fallback(default_return=None)
def add_owner_transfer_error_rate_limit(email: str) -> None:
key = f"owner_transfer_error_rate_limit:{email}"
count = redis_client.get(key)
if count is None:
count = 0
count = int(count) + 1
redis_client.setex(key, dify_config.OWNER_TRANSFER_LOCKOUT_DURATION, count)
@staticmethod
@redis_fallback(default_return=False)
def is_owner_transfer_error_rate_limit(email: str) -> bool:
key = f"owner_transfer_error_rate_limit:{email}"
count = redis_client.get(key)
if count is None:
return False
count = int(count)
if count > AccountService.OWNER_TRANSFER_MAX_ERROR_LIMITS:
return True
return False
@staticmethod
@redis_fallback(default_return=None)
def reset_owner_transfer_error_rate_limit(email: str):
key = f"owner_transfer_error_rate_limit:{email}"
redis_client.delete(key)
@staticmethod
@redis_fallback(default_return=False)
def is_email_send_ip_limit(ip_address: str):
@@ -593,6 +805,10 @@ class AccountService:
return False
@staticmethod
def check_email_unique(email: str) -> bool:
return db.session.query(Account).filter_by(email=email).first() is None
class TenantService:
@staticmethod
@@ -865,6 +1081,15 @@ class TenantService:
return cast(dict, tenant.custom_config_dict)
@staticmethod
def is_owner(account: Account, tenant: Tenant) -> bool:
return TenantService.get_user_role(account, tenant) == TenantAccountRole.OWNER
@staticmethod
def is_member(account: Account, tenant: Tenant) -> bool:
"""Check if the account is a member of the tenant"""
return TenantService.get_user_role(account, tenant) is not None
class RegisterService:
@classmethod

View File

@@ -123,7 +123,7 @@ class FeatureModel(BaseModel):
dataset_operator_enabled: bool = False
webapp_copyright_enabled: bool = False
workspace_members: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
is_allow_transfer_workspace: bool = True
# pydantic configs
model_config = ConfigDict(protected_namespaces=())
@@ -149,6 +149,7 @@ class SystemFeatureModel(BaseModel):
branding: BrandingModel = BrandingModel()
webapp_auth: WebAppAuthModel = WebAppAuthModel()
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
enable_change_email: bool = True
class FeatureService:
@@ -186,6 +187,7 @@ class FeatureService:
if dify_config.ENTERPRISE_ENABLED:
system_features.branding.enabled = True
system_features.webapp_auth.enabled = True
system_features.enable_change_email = False
cls._fulfill_params_from_enterprise(system_features)
if dify_config.MARKETPLACE_ENABLED:
@@ -228,6 +230,8 @@ class FeatureService:
if features.billing.subscription.plan != "sandbox":
features.webapp_copyright_enabled = True
else:
features.is_allow_transfer_workspace = False
if "members" in billing_info:
features.members.size = billing_info["members"]["size"]

View File

@@ -0,0 +1,78 @@
import logging
import time
import click
from celery import shared_task # type: ignore
from flask import render_template
from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail")
def send_change_mail_task(language: str, to: str, code: str, phase: str):
"""
Async Send change email mail
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
:param to: Recipient email address
:param code: Change email code
:param phase: Change email phase (new_email, old_email)
"""
if not mail.is_inited():
return
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
start_at = time.perf_counter()
email_config = {
"zh-Hans": {
"old_email": {
"subject": "检测您现在的邮箱",
"template_with_brand": "change_mail_confirm_old_template_zh-CN.html",
"template_without_brand": "without-brand/change_mail_confirm_old_template_zh-CN.html",
},
"new_email": {
"subject": "确认您的邮箱地址变更",
"template_with_brand": "change_mail_confirm_new_template_zh-CN.html",
"template_without_brand": "without-brand/change_mail_confirm_new_template_zh-CN.html",
},
},
"en": {
"old_email": {
"subject": "Check your current email",
"template_with_brand": "change_mail_confirm_old_template_en-US.html",
"template_without_brand": "without-brand/change_mail_confirm_old_template_en-US.html",
},
"new_email": {
"subject": "Confirm your new email address",
"template_with_brand": "change_mail_confirm_new_template_en-US.html",
"template_without_brand": "without-brand/change_mail_confirm_new_template_en-US.html",
},
},
}
# send change email mail using different languages
try:
system_features = FeatureService.get_system_features()
lang_key = "zh-Hans" if language == "zh-Hans" else "en"
if phase not in ["old_email", "new_email"]:
raise ValueError("Invalid phase")
config = email_config[lang_key][phase]
subject = config["subject"]
if system_features.branding.enabled:
template = config["template_without_brand"]
else:
template = config["template_with_brand"]
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject=subject, html=html_content)
end_at = time.perf_counter()
logging.info(
click.style("Send change email mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green")
)
except Exception:
logging.exception("Send change email mail to {} failed".format(to))

View File

@@ -0,0 +1,152 @@
import logging
import time
import click
from celery import shared_task # type: ignore
from flask import render_template
from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail")
def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspace: str):
"""
Async Send owner transfer confirm mail
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
:param to: Recipient email address
:param workspace: Workspace name
"""
if not mail.is_inited():
return
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
start_at = time.perf_counter()
# send change email mail using different languages
try:
if language == "zh-Hans":
template = "transfer_workspace_owner_confirm_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
template = "without-brand/transfer_workspace_owner_confirm_template_zh-CN.html"
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content)
else:
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content)
else:
template = "transfer_workspace_owner_confirm_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
template = "without-brand/transfer_workspace_owner_confirm_template_en-US.html"
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content)
else:
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content)
end_at = time.perf_counter()
logging.info(
click.style(
"Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at),
fg="green",
)
)
except Exception:
logging.exception("owner transfer confirm email mail to {} failed".format(to))
@shared_task(queue="mail")
def send_old_owner_transfer_notify_email_task(language: str, to: str, workspace: str, new_owner_email: str):
"""
Async Send owner transfer confirm mail
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
:param to: Recipient email address
:param workspace: Workspace name
:param new_owner_email: New owner email
"""
if not mail.is_inited():
return
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
start_at = time.perf_counter()
# send change email mail using different languages
try:
if language == "zh-Hans":
template = "transfer_workspace_old_owner_notify_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
template = "without-brand/transfer_workspace_old_owner_notify_template_zh-CN.html"
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
mail.send(to=to, subject="工作区所有权已转移", html=html_content)
else:
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
mail.send(to=to, subject="工作区所有权已转移", html=html_content)
else:
template = "transfer_workspace_old_owner_notify_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
template = "without-brand/transfer_workspace_old_owner_notify_template_en-US.html"
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
mail.send(to=to, subject="Workspace ownership has been transferred", html=html_content)
else:
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
mail.send(to=to, subject="Workspace ownership has been transferred", html=html_content)
end_at = time.perf_counter()
logging.info(
click.style(
"Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at),
fg="green",
)
)
except Exception:
logging.exception("owner transfer confirm email mail to {} failed".format(to))
@shared_task(queue="mail")
def send_new_owner_transfer_notify_email_task(language: str, to: str, workspace: str):
"""
Async Send owner transfer confirm mail
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
:param to: Recipient email address
:param code: Change email code
:param workspace: Workspace name
"""
if not mail.is_inited():
return
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
start_at = time.perf_counter()
# send change email mail using different languages
try:
if language == "zh-Hans":
template = "transfer_workspace_new_owner_notify_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
template = "without-brand/transfer_workspace_new_owner_notify_template_zh-CN.html"
html_content = render_template(template, to=to, WorkspaceName=workspace)
mail.send(to=to, subject=f"您现在是 {workspace} 的所有者", html=html_content)
else:
html_content = render_template(template, to=to, WorkspaceName=workspace)
mail.send(to=to, subject=f"您现在是 {workspace} 的所有者", html=html_content)
else:
template = "transfer_workspace_new_owner_notify_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
template = "without-brand/transfer_workspace_new_owner_notify_template_en-US.html"
html_content = render_template(template, to=to, WorkspaceName=workspace)
mail.send(to=to, subject=f"You are now the owner of {workspace}", html=html_content)
else:
html_content = render_template(template, to=to, WorkspaceName=workspace)
mail.send(to=to, subject=f"You are now the owner of {workspace}", html=html_content)
end_at = time.perf_counter()
logging.info(
click.style(
"Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at),
fg="green",
)
)
except Exception:
logging.exception("owner transfer confirm email mail to {} failed".format(to))

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">Confirm Your New Email Address</p>
<div class="description">
<p class="content1">Youre updating the email address linked to your Dify account.</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>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didnt make this request, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">确认您的邮箱地址变更</p>
<div class="description">
<p class="content1">您正在更新与您的 Dify 账户关联的邮箱地址。</p>
<p class="content2">为了确认此操作,请使用以下验证码。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效</p>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求变更邮箱地址,请忽略此邮件或立即联系支持。</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">Verify Your Request to Change Email</p>
<div class="description">
<p class="content1">We received a request to change the email address associated with your Dify account.</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>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didnt make this request, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">验证您的邮箱变更请求</p>
<div class="description">
<p class="content1">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p>
<p class="content2">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效</p>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求变更邮箱地址,请忽略此邮件或立即联系支持。</p>
</div>
</body>
</html>

View File

@@ -6,94 +6,136 @@
<title>Documents Disabled Notification</title>
<style>
body {
font-family: Arial, sans-serif;
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.email-container {
max-width: 600px;
margin: 20px auto;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
.container {
width: 504px;
min-height: 638px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
background-color: #eef2fa;
padding: 20px;
text-align: center;
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
height: 40px;
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.button {
display: inline-block;
width: 480px;
padding: 8px 12px;
color: white;
text-decoration: none;
border-radius: 10px;
text-align: center;
transition: background-color 0.3s ease;
border: 0.5px solid rgba(16, 24, 40, 0.04);
background-color: #155AEF;
box-shadow: 0px -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0px 0px 1px 0px rgba(255, 255, 255, 0.16) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset, 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 1px 1px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 0.5px rgba(9, 9, 11, 0.05);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.button:hover {
background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
}
.content {
padding: 20px;
line-height: 1.6;
color: #333;
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content h1 {
font-size: 24px;
color: #222;
.content1 {
margin: 0;
padding-top: 24px;
padding-bottom: 12px;
font-weight: 500;
}
.content p {
margin: 10px 0;
.content2 {
margin: 0;
padding-bottom: 12px;
}
.content ul {
padding-left: 20px;
.list {
margin: 0;
margin-bottom: 20px;
padding: 16px 24px;
border-radius: 16px;
background-color: #F2F4F7;
list-style-type: none;
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content ul li {
margin-bottom: 10px;
.list li {
margin-bottom: 4px;
}
.cta-button, .cta-button:hover, .cta-button:active, .cta-button:visited, .cta-button:focus {
display: block;
margin: 20px auto;
padding: 10px 20px;
background-color: #4e89f9;
color: #ffffff !important;
text-align: center;
text-decoration: none !important;
border-radius: 5px;
width: fit-content;
}
.footer {
text-align: center;
padding: 10px;
font-size: 12px;
color: #777;
background-color: #f9f9f9;
.list li:last-of-type {
margin-bottom: 0px;
}
</style>
</head>
<body>
<div class="email-container">
<div class="container">
<!-- Header -->
<div class="header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
</div>
<!-- Content -->
<h1 class="title">Some Documents in Your Knowledge Base Have Been Disabled</h1>
<div class="content">
<h1>Some Documents in Your Knowledge Base Have Been Disabled</h1>
<p>Dear {{userName}},</p>
<p>
<p class="content1">Dear {{userName}},</p>
<p class="content2">
We're sorry for the inconvenience. To ensure optimal performance, documents
that havent been updated or accessed in the past 30 days have been disabled in
your knowledge bases:
</p>
<ul>
<ul class="list">
{% for item in knowledge_details %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<p>You can re-enable them anytime.</p>
<a href={{url}} class="cta-button">Re-enable in Dify</a>
</div>
<!-- Footer -->
<div class="footer">
Sincerely,<br>
The Dify Team
<p class="content2">You can re-enable them anytime.</p>
<p style="text-align: center; margin: 0; margin-bottom: 44px;">
<a href={{url}} class="button">Re-enable in Dify</a>
</p>
<p class="content2">Best regards,</p>
<p class="content2">Dify Team</p>
</div>
</div>
</body>

View File

@@ -1,73 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 444px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.button {
display: inline-block;
width: 480px;
padding: 8px 12px;
color: white;
text-decoration: none;
border-radius: 10px;
text-align: center;
transition: background-color 0.3s ease;
border: 0.5px solid rgba(16, 24, 40, 0.04);
background-color: #155AEF;
box-shadow: 0px -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0px 0px 1px 0px rgba(255, 255, 255, 0.16) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset, 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 1px 1px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 0.5px rgba(9, 9, 11, 0.05);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.button:hover {
background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
}
.content {
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 24px;
padding-bottom: 12px;
font-weight: 500;
}
.content2 {
margin: 0;
padding-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- 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">
<p>Dear {{ to }},</p>
<p>{{ inviter_name }} is pleased to invite you to join our workspace on Dify, a platform specifically designed for LLM application development. On Dify, you can explore, create, and collaborate to build and operate AI applications.</p>
<p>Click the button below to log in to Dify and join the workspace.</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p>
</div>
<div class="footer">
<p>Best regards,</p>
<p>Dify Team</p>
<p>Please do not reply directly to this email; it is automatically sent by the system.</p>
</div>
<div class="container">
<div class="header">
<!-- 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">
<p class="content1">Dear {{ to }},</p>
<p class="content2">{{ inviter_name }} is pleased to invite you to join our workspace on Dify, a platform specifically designed for LLM application development. On Dify, you can explore, create, and collaborate to build and operate AI applications.</p>
<p class="content2">Click the button below to log in to Dify 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 class="content2">Best regards,</p>
<p class="content2">Dify Team</p>
</div>
</div>
</body>
</html>

View File

@@ -1,72 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 444px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.button {
display: inline-block;
width: 480px;
padding: 8px 12px;
color: white;
text-decoration: none;
border-radius: 10px;
text-align: center;
transition: background-color 0.3s ease;
border: 0.5px solid rgba(16, 24, 40, 0.04);
background-color: #155AEF;
box-shadow: 0px -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0px 0px 1px 0px rgba(255, 255, 255, 0.16) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset, 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 1px 1px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 0.5px rgba(9, 9, 11, 0.05);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.button:hover {
background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
}
.content {
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 24px;
padding-bottom: 12px;
font-weight: 500;
}
.content2 {
margin: 0;
padding-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
</div>
<div class="content">
<p>尊敬的 {{ to }}</p>
<p>{{ inviter_name }} 现邀请您加入我们在 Dify 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 Dify 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
<p>点击下方按钮即可登录 Dify 并且加入空间。</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
</div>
<div class="footer">
<p>此致,</p>
<p>Dify 团队</p>
<p>请不要直接回复此电子邮件;由系统自动发送。</p>
</div>
<div class="container">
<div class="header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
</div>
<div class="content">
<p class="content1">尊敬的 {{ to }}</p>
<p class="content2">{{ inviter_name }} 现邀请您加入我们在 Dify 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 Dify 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
<p class="content2">点击下方按钮即可登录 Dify 并且加入空间。</p>
<p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
<p class="content2">此致,</p>
<p class="content2">Dify 团队</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 374px;
margin: 40px auto;
padding: 0 48px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">You are now the owner of {{WorkspaceName}}</p>
<div class="description">
<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="content3">If you have any questions, please contact support@dify.ai.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 374px;
margin: 40px auto;
padding: 0 48px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">您现在是 {{WorkspaceName}} 的所有者</p>
<div class="description">
<p class="content1">您已被分配为工作空间“{{WorkspaceName}}”的新所有者。</p>
<p class="content2">作为新所有者,您现在对该工作空间拥有完全的管理权限。</p>
<p class="content3">如果您有任何问题请联系support@dify.ai。</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 394px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">Workspace ownership has been transferred</p>
<div class="description">
<p class="content1">You have successfully transferred ownership of the workspace "{{WorkspaceName}}" to {{NewOwnerEmail}}.</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@dify.ai immediately.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 394px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">工作区所有权已转移</p>
<div class="description">
<p class="content1">您已成功将工作空间“{{WorkspaceName}}”的所有权转移给{{NewOwnerEmail}}。</p>
<p class="content2">您不再拥有此工作空间的拥有者权限。您的访问级别已更改为管理员。</p>
<p class="content3">如果您没有发起此转移或对此变更有任何疑问请立即联系support@dify.ai。</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 600px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.warning {
padding-top: 12px;
padding-bottom: 4px;
color: #101828;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.warningList {
margin: 0;
padding-left: 21px;
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">Verify Your Request to Transfer Workspace Ownership</p>
<div class="description">
<p class="content1">We received a request to transfer ownership of your workspace “{{WorkspaceName}}”.</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>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<div class="warning">Please note:</div>
<ul class="warningList">
<li>The ownership transfer will take effect immediately once confirmed and cannot be undone.</li>
<li>Youll become a admin member, and the new owner will have full control of the workspace.</li>
</ul>
<p class="tips">If you didnt make this request, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 600px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.warning {
padding-top: 12px;
padding-bottom: 4px;
color: #101828;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.warningList {
margin: 0;
padding-left: 21px;
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">验证您的工作空间所有权转移请求</p>
<div class="description">
<p class="content1">我们收到了将您的工作空间“{{WorkspaceName}}”的所有权转移的请求。</p>
<p class="content2">为了确认此操作,请使用以下验证码。</p>
<p class="content3">此验证码仅在5分钟内有效</p>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<div class="warning">请注意:</div>
<ul class="warningList">
<li>所有权转移一旦确认将立即生效且无法撤销。</li>
<li>您将成为管理员成员,新的所有者将拥有工作空间的完全控制权。</li>
</ul>
<p class="tips">如果您没有发起此请求,请忽略此邮件或立即联系客服。</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">Confirm Your New Email Address</p>
<div class="description">
<p class="content1">Youre updating the email address linked to your Dify account.</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>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didnt make this request, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">确认您的邮箱地址变更</p>
<div class="description">
<p class="content1">您正在更新与您的 Dify 账户关联的邮箱地址。</p>
<p class="content2">为了确认此操作,请使用以下验证码。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效</p>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求变更邮箱地址,请忽略此邮件或立即联系支持。</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">Verify Your Request to Change Email</p>
<div class="description">
<p class="content1">We received a request to change the email address associated with your Dify account.</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>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didnt make this request, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 454px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">验证您的邮箱变更请求</p>
<div class="description">
<p class="content1">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p>
<p class="content2">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效</p>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求变更邮箱地址,请忽略此邮件或立即联系支持。</p>
</div>
</body>
</html>

View File

@@ -1,69 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 444px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.button {
display: inline-block;
width: 480px;
padding: 8px 12px;
color: white;
text-decoration: none;
border-radius: 10px;
text-align: center;
transition: background-color 0.3s ease;
border: 0.5px solid rgba(16, 24, 40, 0.04);
background-color: #155AEF;
box-shadow: 0px -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0px 0px 1px 0px rgba(255, 255, 255, 0.16) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset, 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 1px 1px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 0.5px rgba(9, 9, 11, 0.05);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.button:hover {
background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
}
.content {
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 24px;
padding-bottom: 12px;
font-weight: 500;
}
.content2 {
margin: 0;
padding-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>Dear {{ to }},</p>
<p>{{ 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>Click the button below to log in to {{application_title}} and join the workspace.</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p>
</div>
<div class="footer">
<p>Best regards,</p>
<p>{{application_title}} Team</p>
<p>Please do not reply directly to this email; it is automatically sent by the system.</p>
</div>
<div class="container">
<div class="header">
<!-- 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">
<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">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 class="content2">Best regards,</p>
<p class="content2">{{application_title}} Team</p>
</div>
</div>
</body>
</html>

View File

@@ -1,69 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 444px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.button {
display: inline-block;
width: 480px;
padding: 8px 12px;
color: white;
text-decoration: none;
border-radius: 10px;
text-align: center;
transition: background-color 0.3s ease;
border: 0.5px solid rgba(16, 24, 40, 0.04);
background-color: #155AEF;
box-shadow: 0px -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0px 0px 1px 0px rgba(255, 255, 255, 0.16) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset, 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 1px 1px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 0.5px rgba(9, 9, 11, 0.05);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.button:hover {
background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
}
.content {
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 24px;
padding-bottom: 12px;
font-weight: 500;
}
.content2 {
margin: 0;
padding-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>尊敬的 {{ to }}</p>
<p>{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
<p>点击下方按钮即可登录 {{application_title}} 并且加入空间</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
</div>
<div class="footer">
<p>此致,</p>
<p>{{application_title}} 团队</p>
<p>请不要直接回复此电子邮件;由系统自动发送。</p>
</div>
<div class="container">
<div class="header"></div>
<div class="content">
<p class="content1">尊敬的 {{ to }}</p>
<p class="content2">{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用</p>
<p class="content2">点击下方按钮即可登录 {{application_title}} 并且加入空间。</p>
<p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
<p class="content2">此致,</p>
<p class="content2">{{application_title}} 团队</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 374px;
margin: 40px auto;
padding: 0 48px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">You are now the owner of {{WorkspaceName}}</p>
<div class="description">
<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="content3">If you have any questions, please contact support@dify.ai.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 374px;
margin: 40px auto;
padding: 0 48px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">您现在是 {{WorkspaceName}} 的所有者</p>
<div class="description">
<p class="content1">您已被分配为工作空间“{{WorkspaceName}}”的新所有者。</p>
<p class="content2">作为新所有者,您现在对该工作空间拥有完全的管理权限。</p>
<p class="content3">如果您有任何问题请联系support@dify.ai。</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 394px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">Workspace ownership has been transferred</p>
<div class="description">
<p class="content1">You have successfully transferred ownership of the workspace "{{WorkspaceName}}" to {{NewOwnerEmail}}.</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@dify.ai immediately.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 394px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">工作区所有权已转移</p>
<div class="description">
<p class="content1">您已成功将工作空间“{{WorkspaceName}}”的所有权转移给{{NewOwnerEmail}}。</p>
<p class="content2">您不再拥有此工作空间的拥有者权限。您的访问级别已更改为管理员。</p>
<p class="content3">如果您没有发起此转移或对此变更有任何疑问请立即联系support@dify.ai。</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 600px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.warning {
padding-top: 12px;
padding-bottom: 4px;
color: #101828;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.warningList {
margin: 0;
padding-left: 21px;
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">Verify Your Request to Transfer Workspace Ownership</p>
<div class="description">
<p class="content1">We received a request to transfer ownership of your workspace “{{WorkspaceName}}”.</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>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<div class="warning">Please note:</div>
<ul class="warningList">
<li>The ownership transfer will take effect immediately once confirmed and cannot be undone.</li>
<li>Youll become a admin member, and the new owner will have full control of the workspace.</li>
</ul>
<p class="tips">If you didnt make this request, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
height: 600px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.warning {
padding-top: 12px;
padding-bottom: 4px;
color: #101828;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
}
.warningList {
margin: 0;
padding-left: 21px;
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">验证您的工作空间所有权转移请求</p>
<div class="description">
<p class="content1">我们收到了将您的工作空间“{{WorkspaceName}}”的所有权转移的请求。</p>
<p class="content2">为了确认此操作,请使用以下验证码。</p>
<p class="content3">此验证码仅在5分钟内有效</p>
</div>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<div class="warning">请注意:</div>
<ul class="warningList">
<li>所有权转移一旦确认将立即生效且无法撤销。</li>
<li>您将成为管理员成员,新的所有者将拥有工作空间的完全控制权。</li>
</ul>
<p class="tips">如果您没有发起此请求,请忽略此邮件或立即联系客服。</p>
</div>
</body>
</html>

View File

@@ -203,6 +203,8 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
# Reset password token expiry minutes
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
CREATE_TIDB_SERVICE_JOB_ENABLED=false