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:
@@ -495,6 +495,8 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
|
|||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
||||||
|
|
||||||
|
@@ -31,6 +31,15 @@ class SecurityConfig(BaseSettings):
|
|||||||
description="Duration in minutes for which a password reset token remains valid",
|
description="Duration in minutes for which a password reset token remains valid",
|
||||||
default=5,
|
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(
|
LOGIN_DISABLED: bool = Field(
|
||||||
description="Whether to disable login checks",
|
description="Whether to disable login checks",
|
||||||
@@ -614,6 +623,16 @@ class AuthConfig(BaseSettings):
|
|||||||
default=86400,
|
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):
|
class ModerationConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
@@ -31,6 +31,18 @@ class PasswordResetRateLimitExceededError(BaseHTTPException):
|
|||||||
code = 429
|
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):
|
class EmailCodeError(BaseHTTPException):
|
||||||
error_code = "email_code_error"
|
error_code = "email_code_error"
|
||||||
description = "Email code is invalid or expired."
|
description = "Email code is invalid or expired."
|
||||||
@@ -65,3 +77,39 @@ class EmailPasswordResetLimitError(BaseHTTPException):
|
|||||||
error_code = "email_password_reset_limit"
|
error_code = "email_password_reset_limit"
|
||||||
description = "Too many failed password reset attempts. Please try again in 24 hours."
|
description = "Too many failed password reset attempts. Please try again in 24 hours."
|
||||||
code = 429
|
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
|
||||||
|
@@ -4,10 +4,20 @@ import pytz
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, fields, marshal_with, reqparse
|
from flask_restful import Resource, fields, marshal_with, reqparse
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
from controllers.console import api
|
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 (
|
from controllers.console.workspace.error import (
|
||||||
AccountAlreadyInitedError,
|
AccountAlreadyInitedError,
|
||||||
CurrentPasswordIncorrectError,
|
CurrentPasswordIncorrectError,
|
||||||
@@ -18,15 +28,17 @@ from controllers.console.workspace.error import (
|
|||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_enabled,
|
cloud_edition_billing_enabled,
|
||||||
|
enable_change_email,
|
||||||
enterprise_license_required,
|
enterprise_license_required,
|
||||||
only_edition_cloud,
|
only_edition_cloud,
|
||||||
setup_required,
|
setup_required,
|
||||||
)
|
)
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.member_fields import account_fields
|
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 libs.login import login_required
|
||||||
from models import AccountIntegrate, InvitationCode
|
from models import AccountIntegrate, InvitationCode
|
||||||
|
from models.account import Account
|
||||||
from services.account_service import AccountService
|
from services.account_service import AccountService
|
||||||
from services.billing_service import BillingService
|
from services.billing_service import BillingService
|
||||||
from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError
|
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"])
|
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
|
# Register API resources
|
||||||
api.add_resource(AccountInitApi, "/account/init")
|
api.add_resource(AccountInitApi, "/account/init")
|
||||||
api.add_resource(AccountProfileApi, "/account/profile")
|
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(EducationVerifyApi, "/account/education/verify")
|
||||||
api.add_resource(EducationApi, "/account/education")
|
api.add_resource(EducationApi, "/account/education")
|
||||||
api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete")
|
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(AccountEmailApi, '/account/email')
|
||||||
# api.add_resource(AccountEmailVerifyApi, '/account/email-verify')
|
# api.add_resource(AccountEmailVerifyApi, '/account/email-verify')
|
||||||
|
@@ -1,22 +1,34 @@
|
|||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, abort, marshal_with, reqparse
|
from flask_restful import Resource, abort, marshal_with, reqparse
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console import api
|
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 (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_resource_check,
|
cloud_edition_billing_resource_check,
|
||||||
|
is_allow_transfer_owner,
|
||||||
setup_required,
|
setup_required,
|
||||||
)
|
)
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.member_fields import account_with_role_list_fields
|
from fields.member_fields import account_with_role_list_fields
|
||||||
|
from libs.helper import extract_remote_ip
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models.account import Account, TenantAccountRole
|
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.errors.account import AccountAlreadyInTenantError
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
@@ -156,8 +168,148 @@ class DatasetOperatorMemberListApi(Resource):
|
|||||||
return {"result": "success", "accounts": members}, 200
|
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(MemberListApi, "/workspaces/current/members")
|
||||||
api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email")
|
api.add_resource(MemberInviteEmailApi, "/workspaces/current/members/invite-email")
|
||||||
api.add_resource(MemberCancelInviteApi, "/workspaces/current/members/<uuid:member_id>")
|
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(MemberUpdateRoleApi, "/workspaces/current/members/<uuid:member_id>/update-role")
|
||||||
api.add_resource(DatasetOperatorMemberListApi, "/workspaces/current/dataset-operators")
|
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")
|
||||||
|
@@ -235,3 +235,29 @@ def email_password_login_enabled(view):
|
|||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return decorated
|
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
|
||||||
|
@@ -52,8 +52,14 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError, Workspaces
|
|||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
from tasks.delete_account_task import delete_account_task
|
from tasks.delete_account_task import delete_account_task
|
||||||
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
|
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_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_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
|
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(
|
email_code_account_deletion_rate_limiter = RateLimiter(
|
||||||
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
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
|
LOGIN_MAX_ERROR_LIMITS = 5
|
||||||
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
||||||
|
CHANGE_EMAIL_MAX_ERROR_LIMITS = 5
|
||||||
|
OWNER_TRANSFER_MAX_ERROR_LIMITS = 5
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_refresh_token_key(refresh_token: str) -> str:
|
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)
|
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
||||||
return token
|
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
|
@classmethod
|
||||||
def generate_reset_password_token(
|
def generate_reset_password_token(
|
||||||
cls,
|
cls,
|
||||||
@@ -435,14 +541,64 @@ class AccountService:
|
|||||||
)
|
)
|
||||||
return code, token
|
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
|
@classmethod
|
||||||
def revoke_reset_password_token(cls, token: str):
|
def revoke_reset_password_token(cls, token: str):
|
||||||
TokenManager.revoke_token(token, "reset_password")
|
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
|
@classmethod
|
||||||
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
|
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
|
||||||
return TokenManager.get_token_data(token, "reset_password")
|
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
|
@classmethod
|
||||||
def send_email_code_login_email(
|
def send_email_code_login_email(
|
||||||
cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US"
|
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}"
|
key = f"forgot_password_error_rate_limit:{email}"
|
||||||
redis_client.delete(key)
|
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
|
@staticmethod
|
||||||
@redis_fallback(default_return=False)
|
@redis_fallback(default_return=False)
|
||||||
def is_email_send_ip_limit(ip_address: str):
|
def is_email_send_ip_limit(ip_address: str):
|
||||||
@@ -593,6 +805,10 @@ class AccountService:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_email_unique(email: str) -> bool:
|
||||||
|
return db.session.query(Account).filter_by(email=email).first() is None
|
||||||
|
|
||||||
|
|
||||||
class TenantService:
|
class TenantService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -865,6 +1081,15 @@ class TenantService:
|
|||||||
|
|
||||||
return cast(dict, tenant.custom_config_dict)
|
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:
|
class RegisterService:
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@@ -123,7 +123,7 @@ class FeatureModel(BaseModel):
|
|||||||
dataset_operator_enabled: bool = False
|
dataset_operator_enabled: bool = False
|
||||||
webapp_copyright_enabled: bool = False
|
webapp_copyright_enabled: bool = False
|
||||||
workspace_members: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
workspace_members: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
||||||
|
is_allow_transfer_workspace: bool = True
|
||||||
# pydantic configs
|
# pydantic configs
|
||||||
model_config = ConfigDict(protected_namespaces=())
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
@@ -149,6 +149,7 @@ class SystemFeatureModel(BaseModel):
|
|||||||
branding: BrandingModel = BrandingModel()
|
branding: BrandingModel = BrandingModel()
|
||||||
webapp_auth: WebAppAuthModel = WebAppAuthModel()
|
webapp_auth: WebAppAuthModel = WebAppAuthModel()
|
||||||
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
|
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
|
||||||
|
enable_change_email: bool = True
|
||||||
|
|
||||||
|
|
||||||
class FeatureService:
|
class FeatureService:
|
||||||
@@ -186,6 +187,7 @@ class FeatureService:
|
|||||||
if dify_config.ENTERPRISE_ENABLED:
|
if dify_config.ENTERPRISE_ENABLED:
|
||||||
system_features.branding.enabled = True
|
system_features.branding.enabled = True
|
||||||
system_features.webapp_auth.enabled = True
|
system_features.webapp_auth.enabled = True
|
||||||
|
system_features.enable_change_email = False
|
||||||
cls._fulfill_params_from_enterprise(system_features)
|
cls._fulfill_params_from_enterprise(system_features)
|
||||||
|
|
||||||
if dify_config.MARKETPLACE_ENABLED:
|
if dify_config.MARKETPLACE_ENABLED:
|
||||||
@@ -228,6 +230,8 @@ class FeatureService:
|
|||||||
|
|
||||||
if features.billing.subscription.plan != "sandbox":
|
if features.billing.subscription.plan != "sandbox":
|
||||||
features.webapp_copyright_enabled = True
|
features.webapp_copyright_enabled = True
|
||||||
|
else:
|
||||||
|
features.is_allow_transfer_workspace = False
|
||||||
|
|
||||||
if "members" in billing_info:
|
if "members" in billing_info:
|
||||||
features.members.size = billing_info["members"]["size"]
|
features.members.size = billing_info["members"]["size"]
|
||||||
|
78
api/tasks/mail_change_mail_task.py
Normal file
78
api/tasks/mail_change_mail_task.py
Normal 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))
|
152
api/tasks/mail_owner_transfer_task.py
Normal file
152
api/tasks/mail_owner_transfer_task.py
Normal 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))
|
125
api/templates/change_mail_confirm_new_template_en-US.html
Normal file
125
api/templates/change_mail_confirm_new_template_en-US.html
Normal 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">You’re 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 didn’t make this request, please ignore this email or contact support immediately.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
125
api/templates/change_mail_confirm_new_template_zh-CN.html
Normal file
125
api/templates/change_mail_confirm_new_template_zh-CN.html
Normal 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>
|
||||||
|
|
125
api/templates/change_mail_confirm_old_template_en-US.html
Normal file
125
api/templates/change_mail_confirm_old_template_en-US.html
Normal 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 didn’t make this request, please ignore this email or contact support immediately.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
125
api/templates/change_mail_confirm_old_template_zh-CN.html
Normal file
125
api/templates/change_mail_confirm_old_template_zh-CN.html
Normal 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>
|
||||||
|
|
@@ -6,94 +6,136 @@
|
|||||||
<title>Documents Disabled Notification</title>
|
<title>Documents Disabled Notification</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #374151;
|
||||||
|
background-color: #E5E7EB;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
}
|
||||||
.email-container {
|
.container {
|
||||||
max-width: 600px;
|
width: 504px;
|
||||||
margin: 20px auto;
|
min-height: 638px;
|
||||||
background: #ffffff;
|
margin: 40px auto;
|
||||||
border-radius: 10px;
|
padding: 0 48px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
background-color: #fcfcfd;
|
||||||
overflow: hidden;
|
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 {
|
.header {
|
||||||
background-color: #eef2fa;
|
padding-top: 36px;
|
||||||
padding: 20px;
|
padding-bottom: 24px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header img {
|
.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 {
|
.content {
|
||||||
padding: 20px;
|
color: #354052;
|
||||||
line-height: 1.6;
|
font-family: Inter;
|
||||||
color: #333;
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
.content h1 {
|
.content1 {
|
||||||
font-size: 24px;
|
margin: 0;
|
||||||
color: #222;
|
padding-top: 24px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.content p {
|
.content2 {
|
||||||
margin: 10px 0;
|
margin: 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
.content ul {
|
.list {
|
||||||
padding-left: 20px;
|
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 {
|
.list li {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.cta-button, .cta-button:hover, .cta-button:active, .cta-button:visited, .cta-button:focus {
|
.list li:last-of-type {
|
||||||
display: block;
|
margin-bottom: 0px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="email-container">
|
<div class="container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
|
<h1 class="title">Some Documents in Your Knowledge Base Have Been Disabled</h1>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>Some Documents in Your Knowledge Base Have Been Disabled</h1>
|
<p class="content1">Dear {{userName}},</p>
|
||||||
<p>Dear {{userName}},</p>
|
<p class="content2">
|
||||||
<p>
|
|
||||||
We're sorry for the inconvenience. To ensure optimal performance, documents
|
We're sorry for the inconvenience. To ensure optimal performance, documents
|
||||||
that haven’t been updated or accessed in the past 30 days have been disabled in
|
that haven’t been updated or accessed in the past 30 days have been disabled in
|
||||||
your knowledge bases:
|
your knowledge bases:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul class="list">
|
||||||
{% for item in knowledge_details %}
|
{% for item in knowledge_details %}
|
||||||
<li>{{ item }}</li>
|
<li>{{ item }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<p>You can re-enable them anytime.</p>
|
<p class="content2">You can re-enable them anytime.</p>
|
||||||
<a href={{url}} class="cta-button">Re-enable in Dify</a>
|
<p style="text-align: center; margin: 0; margin-bottom: 44px;">
|
||||||
</div>
|
<a href={{url}} class="button">Re-enable in Dify</a>
|
||||||
|
</p>
|
||||||
<!-- Footer -->
|
<p class="content2">Best regards,</p>
|
||||||
<div class="footer">
|
<p class="content2">Dify Team</p>
|
||||||
Sincerely,<br>
|
|
||||||
The Dify Team
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@@ -1,73 +1,94 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Arial', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
line-height: 16pt;
|
line-height: 16pt;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
background-color: #E5E7EB;
|
background-color: #E5E7EB;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 504px;
|
||||||
max-width: 560px;
|
height: 444px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
padding: 20px;
|
padding: 0 48px;
|
||||||
background-color: #F3F4F6;
|
background-color: #fcfcfd;
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
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 {
|
}
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
.header {
|
||||||
}
|
padding-top: 36px;
|
||||||
.header img {
|
padding-bottom: 24px;
|
||||||
max-width: 100px;
|
}
|
||||||
height: auto;
|
|
||||||
}
|
.header img {
|
||||||
.button {
|
max-width: 63px;
|
||||||
display: inline-block;
|
height: auto;
|
||||||
padding: 12px 24px;
|
}
|
||||||
background-color: #2970FF;
|
.button {
|
||||||
color: white;
|
display: inline-block;
|
||||||
text-decoration: none;
|
width: 480px;
|
||||||
border-radius: 4px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
color: white;
|
||||||
transition: background-color 0.3s ease;
|
text-decoration: none;
|
||||||
}
|
border-radius: 10px;
|
||||||
.button:hover {
|
text-align: center;
|
||||||
background-color: #265DD4;
|
transition: background-color 0.3s ease;
|
||||||
}
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
.footer {
|
background-color: #155AEF;
|
||||||
font-size: 0.9em;
|
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);
|
||||||
color: #777777;
|
font-family: Inter;
|
||||||
margin-top: 30px;
|
font-size: 14px;
|
||||||
}
|
font-style: normal;
|
||||||
.content {
|
font-weight: 600;
|
||||||
margin-top: 20px;
|
line-height: 20px; /* 142.857% */
|
||||||
}
|
}
|
||||||
</style>
|
.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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<!-- Optional: Add a logo or a header image here -->
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
|
<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>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,72 +1,93 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Arial', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
line-height: 16pt;
|
line-height: 16pt;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
background-color: #E5E7EB;
|
background-color: #E5E7EB;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 504px;
|
||||||
max-width: 560px;
|
height: 444px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
padding: 20px;
|
padding: 0 48px;
|
||||||
background-color: #F3F4F6;
|
background-color: #fcfcfd;
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
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 {
|
}
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
.header {
|
||||||
}
|
padding-top: 36px;
|
||||||
.header img {
|
padding-bottom: 24px;
|
||||||
max-width: 100px;
|
}
|
||||||
height: auto;
|
|
||||||
}
|
.header img {
|
||||||
.button {
|
max-width: 63px;
|
||||||
display: inline-block;
|
height: auto;
|
||||||
padding: 12px 24px;
|
}
|
||||||
background-color: #2970FF;
|
.button {
|
||||||
color: white;
|
display: inline-block;
|
||||||
text-decoration: none;
|
width: 480px;
|
||||||
border-radius: 4px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
color: white;
|
||||||
transition: background-color 0.3s ease;
|
text-decoration: none;
|
||||||
}
|
border-radius: 10px;
|
||||||
.button:hover {
|
text-align: center;
|
||||||
background-color: #265DD4;
|
transition: background-color 0.3s ease;
|
||||||
}
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
.footer {
|
background-color: #155AEF;
|
||||||
font-size: 0.9em;
|
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);
|
||||||
color: #777777;
|
font-family: Inter;
|
||||||
margin-top: 30px;
|
font-size: 14px;
|
||||||
}
|
font-style: normal;
|
||||||
.content {
|
font-weight: 600;
|
||||||
margin-top: 20px;
|
line-height: 20px; /* 142.857% */
|
||||||
}
|
}
|
||||||
</style>
|
.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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
|
<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>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -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>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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>You’ll become a admin member, and the new owner will have full control of the workspace.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="tips">If you didn’t make this request, please ignore this email or contact support immediately.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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">You’re 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 didn’t make this request, please ignore this email or contact support immediately.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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 didn’t make this request, please ignore this email or contact support immediately.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -1,69 +1,94 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Arial', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
line-height: 16pt;
|
line-height: 16pt;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
background-color: #E5E7EB;
|
background-color: #E5E7EB;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 504px;
|
||||||
max-width: 560px;
|
height: 444px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
padding: 20px;
|
padding: 0 48px;
|
||||||
background-color: #F3F4F6;
|
background-color: #fcfcfd;
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
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 {
|
}
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
.header {
|
||||||
}
|
padding-top: 36px;
|
||||||
.header img {
|
padding-bottom: 24px;
|
||||||
max-width: 100px;
|
}
|
||||||
height: auto;
|
|
||||||
}
|
.header img {
|
||||||
.button {
|
max-width: 63px;
|
||||||
display: inline-block;
|
height: auto;
|
||||||
padding: 12px 24px;
|
}
|
||||||
background-color: #2970FF;
|
.button {
|
||||||
color: white;
|
display: inline-block;
|
||||||
text-decoration: none;
|
width: 480px;
|
||||||
border-radius: 4px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
color: white;
|
||||||
transition: background-color 0.3s ease;
|
text-decoration: none;
|
||||||
}
|
border-radius: 10px;
|
||||||
.button:hover {
|
text-align: center;
|
||||||
background-color: #265DD4;
|
transition: background-color 0.3s ease;
|
||||||
}
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
.footer {
|
background-color: #155AEF;
|
||||||
font-size: 0.9em;
|
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);
|
||||||
color: #777777;
|
font-family: Inter;
|
||||||
margin-top: 30px;
|
font-size: 14px;
|
||||||
}
|
font-style: normal;
|
||||||
.content {
|
font-weight: 600;
|
||||||
margin-top: 20px;
|
line-height: 20px; /* 142.857% */
|
||||||
}
|
}
|
||||||
</style>
|
.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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="content">
|
<div class="header">
|
||||||
<p>Dear {{ to }},</p>
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
<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>
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
|
||||||
<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>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,69 +1,91 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Arial', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
line-height: 16pt;
|
line-height: 16pt;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
background-color: #E5E7EB;
|
background-color: #E5E7EB;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 504px;
|
||||||
max-width: 560px;
|
height: 444px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
padding: 20px;
|
padding: 0 48px;
|
||||||
background-color: #F3F4F6;
|
background-color: #fcfcfd;
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
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 {
|
}
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
.header {
|
||||||
}
|
padding-top: 36px;
|
||||||
.header img {
|
padding-bottom: 24px;
|
||||||
max-width: 100px;
|
}
|
||||||
height: auto;
|
|
||||||
}
|
.header img {
|
||||||
.button {
|
max-width: 63px;
|
||||||
display: inline-block;
|
height: auto;
|
||||||
padding: 12px 24px;
|
}
|
||||||
background-color: #2970FF;
|
.button {
|
||||||
color: white;
|
display: inline-block;
|
||||||
text-decoration: none;
|
width: 480px;
|
||||||
border-radius: 4px;
|
padding: 8px 12px;
|
||||||
text-align: center;
|
color: white;
|
||||||
transition: background-color 0.3s ease;
|
text-decoration: none;
|
||||||
}
|
border-radius: 10px;
|
||||||
.button:hover {
|
text-align: center;
|
||||||
background-color: #265DD4;
|
transition: background-color 0.3s ease;
|
||||||
}
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
.footer {
|
background-color: #155AEF;
|
||||||
font-size: 0.9em;
|
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);
|
||||||
color: #777777;
|
font-family: Inter;
|
||||||
margin-top: 30px;
|
font-size: 14px;
|
||||||
}
|
font-style: normal;
|
||||||
.content {
|
font-weight: 600;
|
||||||
margin-top: 20px;
|
line-height: 20px; /* 142.857% */
|
||||||
}
|
}
|
||||||
</style>
|
.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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="content">
|
<div class="header"></div>
|
||||||
<p>尊敬的 {{ to }},</p>
|
<div class="content">
|
||||||
<p>{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
|
<p class="content1">尊敬的 {{ to }},</p>
|
||||||
<p>点击下方按钮即可登录 {{application_title}} 并且加入空间。</p>
|
<p class="content2">{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
|
||||||
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
|
<p class="content2">点击下方按钮即可登录 {{application_title}} 并且加入空间。</p>
|
||||||
</div>
|
<p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
|
||||||
<div class="footer">
|
<p class="content2">此致,</p>
|
||||||
<p>此致,</p>
|
<p class="content2">{{application_title}} 团队</p>
|
||||||
<p>{{application_title}} 团队</p>
|
|
||||||
<p>请不要直接回复此电子邮件;由系统自动发送。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -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>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -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>You’ll become a admin member, and the new owner will have full control of the workspace.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="tips">If you didn’t make this request, please ignore this email or contact support immediately.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@@ -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>
|
||||||
|
|
@@ -203,6 +203,8 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
|
|||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
||||||
|
|
||||||
|
@@ -772,6 +772,8 @@ INVITE_EXPIRY_HOURS=72
|
|||||||
|
|
||||||
# Reset password token valid time (minutes),
|
# Reset password token valid time (minutes),
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
# The sandbox service endpoint.
|
# The sandbox service endpoint.
|
||||||
CODE_EXECUTION_ENDPOINT=http://sandbox:8194
|
CODE_EXECUTION_ENDPOINT=http://sandbox:8194
|
||||||
|
@@ -335,6 +335,8 @@ x-shared-env: &shared-api-worker-env
|
|||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
||||||
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: ${CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: ${OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194}
|
CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194}
|
||||||
CODE_EXECUTION_API_KEY: ${CODE_EXECUTION_API_KEY:-dify-sandbox}
|
CODE_EXECUTION_API_KEY: ${CODE_EXECUTION_API_KEY:-dify-sandbox}
|
||||||
CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807}
|
CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807}
|
||||||
|
371
web/app/account/account-page/email-change-modal.tsx
Normal file
371
web/app/account/account-page/email-change-modal.tsx
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import {
|
||||||
|
checkEmailExisted,
|
||||||
|
logout,
|
||||||
|
resetEmail,
|
||||||
|
sendVerifyCode,
|
||||||
|
verifyEmail,
|
||||||
|
} from '@/service/common'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
show: boolean
|
||||||
|
onClose: () => void
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
enum STEP {
|
||||||
|
start = 'start',
|
||||||
|
verifyOrigin = 'verifyOrigin',
|
||||||
|
newEmail = 'newEmail',
|
||||||
|
verifyNew = 'verifyNew',
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
const router = useRouter()
|
||||||
|
const [step, setStep] = useState<STEP>(STEP.start)
|
||||||
|
const [code, setCode] = useState<string>('')
|
||||||
|
const [mail, setMail] = useState<string>('')
|
||||||
|
const [time, setTime] = useState<number>(0)
|
||||||
|
const [stepToken, setStepToken] = useState<string>('')
|
||||||
|
const [newEmailExited, setNewEmailExited] = useState<boolean>(false)
|
||||||
|
const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const startCount = () => {
|
||||||
|
setTime(60)
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTime((prev) => {
|
||||||
|
if (prev <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendEmail = async (email: string, isOrigin: boolean, token?: string) => {
|
||||||
|
try {
|
||||||
|
const res = await sendVerifyCode({
|
||||||
|
email,
|
||||||
|
phase: isOrigin ? 'old_email' : 'new_email',
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
startCount()
|
||||||
|
if (res.data)
|
||||||
|
setStepToken(res.data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error sending verification code: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyEmailAddress = async (email: string, code: string, token: string, callback?: (data?: any) => void) => {
|
||||||
|
try {
|
||||||
|
const res = await verifyEmail({
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
if (res.is_valid) {
|
||||||
|
setStepToken(res.token)
|
||||||
|
callback?.(res.token)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Verifying email failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error verifying email: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendCodeToOriginEmail = async () => {
|
||||||
|
await sendEmail(
|
||||||
|
email,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
setStep(STEP.verifyOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerifyOriginEmail = async () => {
|
||||||
|
await verifyEmailAddress(email, code, stepToken, () => setStep(STEP.newEmail))
|
||||||
|
setCode('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidEmail = (email: string): boolean => {
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
return emailRegex.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkNewEmailExisted = async (email: string) => {
|
||||||
|
setIsCheckingEmail(true)
|
||||||
|
try {
|
||||||
|
await checkEmailExisted({
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
setNewEmailExited(false)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
setNewEmailExited(true)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsCheckingEmail(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewEmailValueChange = (mailAddress: string) => {
|
||||||
|
setMail(mailAddress)
|
||||||
|
setNewEmailExited(false)
|
||||||
|
if (isValidEmail(mailAddress))
|
||||||
|
checkNewEmailExisted(mailAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendCodeToNewEmail = async () => {
|
||||||
|
if (!isValidEmail(mail)) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Invalid email format',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sendEmail(
|
||||||
|
mail,
|
||||||
|
false,
|
||||||
|
stepToken,
|
||||||
|
)
|
||||||
|
setStep(STEP.verifyNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout({
|
||||||
|
url: '/logout',
|
||||||
|
params: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
localStorage.removeItem('setup_status')
|
||||||
|
localStorage.removeItem('console_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
|
||||||
|
router.push('/signin')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateEmail = async (lastToken: string) => {
|
||||||
|
try {
|
||||||
|
await resetEmail({
|
||||||
|
new_email: mail,
|
||||||
|
token: lastToken,
|
||||||
|
})
|
||||||
|
handleLogout()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error changing email: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitNewEmail = async () => {
|
||||||
|
await verifyEmailAddress(mail, code, stepToken, updateEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isShow={show}
|
||||||
|
onClose={noop}
|
||||||
|
className='!w-[420px] !p-6'
|
||||||
|
>
|
||||||
|
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
|
||||||
|
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
{step === STEP.start && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.title')}</div>
|
||||||
|
<div className='space-y-0.5 pb-2 pt-1'>
|
||||||
|
<div className='body-md-medium text-text-warning'>{t('common.account.changeEmail.authTip')}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.account.changeEmail.content1"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'></div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={sendCodeToOriginEmail}
|
||||||
|
>
|
||||||
|
{t('common.account.changeEmail.sendVerifyCode')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.verifyOrigin && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyEmail')}</div>
|
||||||
|
<div className='space-y-0.5 pb-2 pt-1'>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.account.changeEmail.content2"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.codeLabel')}</div>
|
||||||
|
<Input
|
||||||
|
className='!w-full'
|
||||||
|
placeholder={t('common.account.changeEmail.codePlaceholder')}
|
||||||
|
value={code}
|
||||||
|
onChange={e => setCode(e.target.value)}
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={code.length !== 6}
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={handleVerifyOriginEmail}
|
||||||
|
>
|
||||||
|
{t('common.account.changeEmail.continue')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||||||
|
<span>{t('common.account.changeEmail.resendTip')}</span>
|
||||||
|
{time > 0 && (
|
||||||
|
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
|
||||||
|
)}
|
||||||
|
{!time && (
|
||||||
|
<span onClick={sendCodeToOriginEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.account.changeEmail.resend')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.newEmail && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.newEmail')}</div>
|
||||||
|
<div className='space-y-0.5 pb-2 pt-1'>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.account.changeEmail.content3')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.emailLabel')}</div>
|
||||||
|
<Input
|
||||||
|
className='!w-full'
|
||||||
|
placeholder={t('common.account.changeEmail.emailPlaceholder')}
|
||||||
|
value={mail}
|
||||||
|
onChange={e => handleNewEmailValueChange(e.target.value)}
|
||||||
|
destructive={newEmailExited}
|
||||||
|
/>
|
||||||
|
{newEmailExited && (
|
||||||
|
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={!mail || newEmailExited || isCheckingEmail || !isValidEmail(mail)}
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={sendCodeToNewEmail}
|
||||||
|
>
|
||||||
|
{t('common.account.changeEmail.sendVerifyCode')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.verifyNew && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyNew')}</div>
|
||||||
|
<div className='space-y-0.5 pb-2 pt-1'>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.account.changeEmail.content4"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email: mail }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.codeLabel')}</div>
|
||||||
|
<Input
|
||||||
|
className='!w-full'
|
||||||
|
placeholder={t('common.account.changeEmail.codePlaceholder')}
|
||||||
|
value={code}
|
||||||
|
onChange={e => setCode(e.target.value)}
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={code.length !== 6}
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={submitNewEmail}
|
||||||
|
>
|
||||||
|
{t('common.account.changeEmail.changeTo', { email: mail })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||||||
|
<span>{t('common.account.changeEmail.resendTip')}</span>
|
||||||
|
{time > 0 && (
|
||||||
|
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
|
||||||
|
)}
|
||||||
|
{!time && (
|
||||||
|
<span onClick={sendCodeToNewEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.account.changeEmail.resend')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmailChangeModal
|
@@ -1,9 +0,0 @@
|
|||||||
.modal {
|
|
||||||
padding: 24px 32px !important;
|
|
||||||
width: 400px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg {
|
|
||||||
background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB;
|
|
||||||
}
|
|
||||||
|
|
@@ -6,7 +6,6 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import DeleteAccount from '../delete-account'
|
import DeleteAccount from '../delete-account'
|
||||||
import s from './index.module.css'
|
|
||||||
import AvatarWithEdit from './AvatarWithEdit'
|
import AvatarWithEdit from './AvatarWithEdit'
|
||||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||||
import type { IItem } from '@/app/components/header/account-setting/collapse'
|
import type { IItem } from '@/app/components/header/account-setting/collapse'
|
||||||
@@ -21,6 +20,7 @@ import { IS_CE_EDITION } from '@/config'
|
|||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import EmailChangeModal from './email-change-modal'
|
||||||
import { validPassword } from '@/config'
|
import { validPassword } from '@/config'
|
||||||
|
|
||||||
const titleClassName = `
|
const titleClassName = `
|
||||||
@@ -47,6 +47,7 @@ export default function AccountPage() {
|
|||||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||||
|
const [showUpdateEmail, setShowUpdateEmail] = useState(false)
|
||||||
|
|
||||||
const handleEditName = () => {
|
const handleEditName = () => {
|
||||||
setEditNameModalVisible(true)
|
setEditNameModalVisible(true)
|
||||||
@@ -122,10 +123,17 @@ export default function AccountPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderAppItem = (item: IItem) => {
|
const renderAppItem = (item: IItem) => {
|
||||||
|
const { icon, icon_background, icon_type, icon_url } = item as any
|
||||||
return (
|
return (
|
||||||
<div className='flex px-3 py-1'>
|
<div className='flex px-3 py-1'>
|
||||||
<div className='mr-3'>
|
<div className='mr-3'>
|
||||||
<AppIcon size='tiny' />
|
<AppIcon
|
||||||
|
size='tiny'
|
||||||
|
iconType={icon_type}
|
||||||
|
icon={icon}
|
||||||
|
background={icon_background}
|
||||||
|
imageUrl={icon_url}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='system-sm-medium mt-[3px] text-text-secondary'>{item.name}</div>
|
<div className='system-sm-medium mt-[3px] text-text-secondary'>{item.name}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +177,11 @@ export default function AccountPage() {
|
|||||||
<div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
|
<div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
|
||||||
<span className='pl-1'>{userProfile.email}</span>
|
<span className='pl-1'>{userProfile.email}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{systemFeatures.enable_change_email && (
|
||||||
|
<div className='system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text' onClick={() => setShowUpdateEmail(true)}>
|
||||||
|
{t('common.operation.change')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
@@ -189,7 +202,7 @@ export default function AccountPage() {
|
|||||||
{!!apps.length && (
|
{!!apps.length && (
|
||||||
<Collapse
|
<Collapse
|
||||||
title={`${t('common.account.showAppLength', { length: apps.length })}`}
|
title={`${t('common.account.showAppLength', { length: apps.length })}`}
|
||||||
items={apps.map(app => ({ key: app.id, name: app.name }))}
|
items={apps.map(app => ({ ...app, key: app.id, name: app.name }))}
|
||||||
renderItem={renderAppItem}
|
renderItem={renderAppItem}
|
||||||
wrapperClassName='mt-2'
|
wrapperClassName='mt-2'
|
||||||
/>
|
/>
|
||||||
@@ -201,7 +214,7 @@ export default function AccountPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
isShow
|
isShow
|
||||||
onClose={() => setEditNameModalVisible(false)}
|
onClose={() => setEditNameModalVisible(false)}
|
||||||
className={s.modal}
|
className='!w-[420px] !p-6'
|
||||||
>
|
>
|
||||||
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{t('common.account.editName')}</div>
|
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{t('common.account.editName')}</div>
|
||||||
<div className={titleClassName}>{t('common.account.name')}</div>
|
<div className={titleClassName}>{t('common.account.name')}</div>
|
||||||
@@ -230,7 +243,7 @@ export default function AccountPage() {
|
|||||||
setEditPasswordModalVisible(false)
|
setEditPasswordModalVisible(false)
|
||||||
resetPasswordForm()
|
resetPasswordForm()
|
||||||
}}
|
}}
|
||||||
className={s.modal}
|
className='!w-[420px] !p-6'
|
||||||
>
|
>
|
||||||
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
|
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
|
||||||
{userProfile.is_password_set && (
|
{userProfile.is_password_set && (
|
||||||
@@ -315,6 +328,13 @@ export default function AccountPage() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{showUpdateEmail && (
|
||||||
|
<EmailChangeModal
|
||||||
|
show={showUpdateEmail}
|
||||||
|
onClose={() => setShowUpdateEmail(false)}
|
||||||
|
email={userProfile.email}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -99,7 +99,8 @@ export type CurrentPlanInfoBackend = {
|
|||||||
workspace_members: {
|
workspace_members: {
|
||||||
size: number
|
size: number
|
||||||
limit: number
|
limit: number
|
||||||
}
|
},
|
||||||
|
is_allow_transfer_workspace: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubscriptionItem = {
|
export type SubscriptionItem = {
|
||||||
|
@@ -10,7 +10,9 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import InviteModal from './invite-modal'
|
import InviteModal from './invite-modal'
|
||||||
import InvitedModal from './invited-modal'
|
import InvitedModal from './invited-modal'
|
||||||
import EditWorkspaceModal from './edit-workspace-modal'
|
import EditWorkspaceModal from './edit-workspace-modal'
|
||||||
|
import TransferOwnershipModal from './transfer-ownership-modal'
|
||||||
import Operation from './operation'
|
import Operation from './operation'
|
||||||
|
import TransferOwnership from './operation/transfer-ownership'
|
||||||
import { fetchMembers } from '@/service/common'
|
import { fetchMembers } from '@/service/common'
|
||||||
import I18n from '@/context/i18n'
|
import I18n from '@/context/i18n'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
@@ -52,10 +54,11 @@ const MembersPage = () => {
|
|||||||
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
||||||
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
const [invitedModalVisible, setInvitedModalVisible] = useState(false)
|
||||||
const accounts = data?.accounts || []
|
const accounts = data?.accounts || []
|
||||||
const { plan, enableBilling } = useProviderContext()
|
const { plan, enableBilling, isAllowTransferWorkspace } = useProviderContext()
|
||||||
const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
|
const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
|
||||||
const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers
|
const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers
|
||||||
const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false)
|
const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false)
|
||||||
|
const [showTransferOwnershipModal, setShowTransferOwnershipModal] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -132,11 +135,18 @@ const MembersPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='system-sm-regular flex w-[104px] shrink-0 items-center py-2 text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
|
<div className='system-sm-regular flex w-[104px] shrink-0 items-center py-2 text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
|
||||||
<div className='flex w-[96px] shrink-0 items-center'>
|
<div className='flex w-[96px] shrink-0 items-center'>
|
||||||
{
|
{isCurrentWorkspaceOwner && account.role === 'owner' && isAllowTransferWorkspace && (
|
||||||
isCurrentWorkspaceOwner && account.role !== 'owner'
|
<TransferOwnership onOperate={() => setShowTransferOwnershipModal(true)}></TransferOwnership>
|
||||||
? <Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
|
)}
|
||||||
: <div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
|
{isCurrentWorkspaceOwner && account.role === 'owner' && !isAllowTransferWorkspace && (
|
||||||
}
|
<div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
|
||||||
|
)}
|
||||||
|
{isCurrentWorkspaceOwner && account.role !== 'owner' && (
|
||||||
|
<Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
|
||||||
|
)}
|
||||||
|
{!isCurrentWorkspaceOwner && (
|
||||||
|
<div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -172,6 +182,12 @@ const MembersPage = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{showTransferOwnershipModal && (
|
||||||
|
<TransferOwnershipModal
|
||||||
|
show={showTransferOwnershipModal}
|
||||||
|
onClose={() => setShowTransferOwnershipModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,54 @@
|
|||||||
|
'use client'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onOperate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransferOwnership = ({ onOperate }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative h-full w-full">
|
||||||
|
{
|
||||||
|
({ open }) => (
|
||||||
|
<>
|
||||||
|
<MenuButton className={cn('system-sm-regular group flex h-full w-full cursor-pointer items-center justify-between px-3 text-text-secondary hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
|
||||||
|
{t('common.members.owner')}
|
||||||
|
<RiArrowDownSLine className={cn('h-4 w-4 group-hover:block', open ? 'block' : 'hidden')} />
|
||||||
|
</MenuButton>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<MenuItems
|
||||||
|
className={cn('absolute right-0 top-[52px] z-10 origin-top-right rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm')}
|
||||||
|
>
|
||||||
|
<div className="p-1">
|
||||||
|
<MenuItem>
|
||||||
|
<div className='flex cursor-pointer rounded-lg px-3 py-2 hover:bg-state-base-hover' onClick={onOperate}>
|
||||||
|
<div className='system-md-regular whitespace-nowrap text-text-secondary'>{t('common.members.transferOwnership')}</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</MenuItems>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransferOwnership
|
@@ -0,0 +1,253 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import MemberSelector from './member-selector'
|
||||||
|
import {
|
||||||
|
ownershipTransfer,
|
||||||
|
sendOwnerEmail,
|
||||||
|
verifyOwnerEmail,
|
||||||
|
} from '@/service/common'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
show: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
enum STEP {
|
||||||
|
start = 'start',
|
||||||
|
verify = 'verify',
|
||||||
|
transfer = 'transfer',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
const { currentWorkspace, userProfile } = useAppContext()
|
||||||
|
const [step, setStep] = useState<STEP>(STEP.start)
|
||||||
|
const [code, setCode] = useState<string>('')
|
||||||
|
const [time, setTime] = useState<number>(0)
|
||||||
|
const [stepToken, setStepToken] = useState<string>('')
|
||||||
|
const [newOwner, setNewOwner] = useState<string>('')
|
||||||
|
const [isTransfer, setIsTransfer] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const startCount = () => {
|
||||||
|
setTime(60)
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTime((prev) => {
|
||||||
|
if (prev <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendEmail = async () => {
|
||||||
|
try {
|
||||||
|
const res = await sendOwnerEmail({})
|
||||||
|
startCount()
|
||||||
|
if (res.data)
|
||||||
|
setStepToken(res.data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error sending verification code: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyEmailAddress = async (code: string, token: string, callback?: () => void) => {
|
||||||
|
try {
|
||||||
|
const res = await verifyOwnerEmail({
|
||||||
|
code,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
if (res.is_valid) {
|
||||||
|
setStepToken(res.token)
|
||||||
|
callback?.()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Verifying email failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error verifying email: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendCodeToOriginEmail = async () => {
|
||||||
|
await sendEmail()
|
||||||
|
setStep(STEP.verify)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerifyOriginEmail = async () => {
|
||||||
|
await verifyEmailAddress(code, stepToken, () => setStep(STEP.transfer))
|
||||||
|
setCode('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTransfer = async () => {
|
||||||
|
setIsTransfer(true)
|
||||||
|
try {
|
||||||
|
await ownershipTransfer(
|
||||||
|
newOwner,
|
||||||
|
{
|
||||||
|
token: stepToken,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
globalThis.location.reload()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Error ownership transfer: ${error ? (error as any).message : ''}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsTransfer(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isShow={show}
|
||||||
|
onClose={noop}
|
||||||
|
className='!w-[420px] !p-6'
|
||||||
|
>
|
||||||
|
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
|
||||||
|
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
{step === STEP.start && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.title')}</div>
|
||||||
|
<div className='space-y-1 pb-2 pt-1'>
|
||||||
|
<div className='body-md-medium text-text-destructive'>{t('common.members.transferModal.warning', { workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.warningTip')}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.members.transferModal.sendTip"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email: userProfile.email }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'></div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={sendCodeToOriginEmail}
|
||||||
|
>
|
||||||
|
{t('common.members.transferModal.sendVerifyCode')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.verify && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.verifyEmail')}</div>
|
||||||
|
<div className='pb-2 pt-1'>
|
||||||
|
<div className='body-md-regular text-text-secondary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.members.transferModal.verifyContent"
|
||||||
|
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
|
||||||
|
values={{ email: userProfile.email }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.verifyContent2')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.members.transferModal.codeLabel')}</div>
|
||||||
|
<Input
|
||||||
|
className='!w-full'
|
||||||
|
placeholder={t('common.members.transferModal.codePlaceholder')}
|
||||||
|
value={code}
|
||||||
|
onChange={e => setCode(e.target.value)}
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={code.length !== 6}
|
||||||
|
className='!w-full'
|
||||||
|
variant='primary'
|
||||||
|
onClick={handleVerifyOriginEmail}
|
||||||
|
>
|
||||||
|
{t('common.members.transferModal.continue')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
|
||||||
|
<span>{t('common.members.transferModal.resendTip')}</span>
|
||||||
|
{time > 0 && (
|
||||||
|
<span>{t('common.members.transferModal.resendCount', { count: time })}</span>
|
||||||
|
)}
|
||||||
|
{!time && (
|
||||||
|
<span onClick={sendCodeToOriginEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.members.transferModal.resend')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === STEP.transfer && (
|
||||||
|
<>
|
||||||
|
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.members.transferModal.title')}</div>
|
||||||
|
<div className='space-y-1 pb-2 pt-1'>
|
||||||
|
<div className='body-md-medium text-text-destructive'>{t('common.members.transferModal.warning', { workspace: currentWorkspace.name.replace(/'/g, '’') })}</div>
|
||||||
|
<div className='body-md-regular text-text-secondary'>{t('common.members.transferModal.warningTip')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='pt-3'>
|
||||||
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.members.transferModal.transferLabel')}</div>
|
||||||
|
<MemberSelector
|
||||||
|
exclude={[userProfile.id]}
|
||||||
|
value={newOwner}
|
||||||
|
onSelect={setNewOwner}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-4 space-y-2'>
|
||||||
|
<Button
|
||||||
|
disabled={!newOwner || isTransfer}
|
||||||
|
className='!w-full'
|
||||||
|
variant='warning'
|
||||||
|
onClick={handleTransfer}
|
||||||
|
>
|
||||||
|
{t('common.members.transferModal.transfer')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='!w-full'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransferOwnershipModal
|
@@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Avatar from '@/app/components/base/avatar'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import { fetchMembers } from '@/service/common'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value?: any
|
||||||
|
onSelect: (value: any) => void
|
||||||
|
exclude?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberSelector: FC<Props> = ({
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
exclude = [],
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
|
||||||
|
const { data } = useSWR(
|
||||||
|
{
|
||||||
|
url: '/workspaces/current/members',
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
fetchMembers,
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentValue = useMemo(() => {
|
||||||
|
if (!data?.accounts) return null
|
||||||
|
const accounts = data.accounts || []
|
||||||
|
if (!value) return null
|
||||||
|
return accounts.find(account => account.id === value)
|
||||||
|
}, [data, value])
|
||||||
|
|
||||||
|
const filteredList = useMemo(() => {
|
||||||
|
if (!data?.accounts) return []
|
||||||
|
const accounts = data.accounts
|
||||||
|
if (!searchValue) return accounts.filter(account => !exclude.includes(account.id))
|
||||||
|
return accounts.filter((account) => {
|
||||||
|
const name = account.name || ''
|
||||||
|
const email = account.email || ''
|
||||||
|
return name.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
|
|| email.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
|
}).filter(account => !exclude.includes(account.id))
|
||||||
|
}, [data, searchValue, exclude])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='bottom'
|
||||||
|
offset={4}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger
|
||||||
|
className='w-full'
|
||||||
|
onClick={() => setOpen(v => !v)}
|
||||||
|
>
|
||||||
|
<div className={cn('group flex cursor-pointer items-center gap-1.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt', open && 'bg-state-base-hover-alt')}>
|
||||||
|
{!currentValue && (
|
||||||
|
<div className='system-sm-regular grow p-1 text-components-input-text-placeholder'>{t('common.members.transferModal.transferPlaceholder')}</div>
|
||||||
|
)}
|
||||||
|
{currentValue && (
|
||||||
|
<>
|
||||||
|
<Avatar avatar={currentValue.avatar_url} size={24} name={currentValue.name} />
|
||||||
|
<div className='system-sm-medium grow truncate text-text-secondary'>{currentValue.name}</div>
|
||||||
|
<div className='system-xs-regular text-text-quaternary'>{currentValue.email}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<RiArrowDownSLine className={cn('h-4 w-4 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[1000]'>
|
||||||
|
<div className='min-w-[372px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
|
||||||
|
<div className='p-2 pb-1'>
|
||||||
|
<Input
|
||||||
|
showLeftIcon
|
||||||
|
value={searchValue}
|
||||||
|
onChange={e => setSearchValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='p-1'>
|
||||||
|
{filteredList.map(account => (
|
||||||
|
<div
|
||||||
|
key={account.id}
|
||||||
|
className='flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-2 pr-3 hover:bg-state-base-hover'
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(account.id)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar avatar={account.avatar_url} size={24} name={account.name} />
|
||||||
|
<div className='system-sm-medium grow truncate text-text-secondary'>{account.name}</div>
|
||||||
|
<div className='system-xs-regular text-text-quaternary'>{account.email}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default MemberSelector
|
@@ -56,6 +56,7 @@ type ProviderContextState = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshLicenseLimit: () => void
|
refreshLicenseLimit: () => void
|
||||||
|
isAllowTransferWorkspace: boolean
|
||||||
}
|
}
|
||||||
const ProviderContext = createContext<ProviderContextState>({
|
const ProviderContext = createContext<ProviderContextState>({
|
||||||
modelProviders: [],
|
modelProviders: [],
|
||||||
@@ -97,6 +98,7 @@ const ProviderContext = createContext<ProviderContextState>({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshLicenseLimit: noop,
|
refreshLicenseLimit: noop,
|
||||||
|
isAllowTransferWorkspace: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useProviderContext = () => useContext(ProviderContext)
|
export const useProviderContext = () => useContext(ProviderContext)
|
||||||
@@ -134,6 +136,7 @@ export const ProviderContextProvider = ({
|
|||||||
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
|
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
|
||||||
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
|
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
|
||||||
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
|
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
|
||||||
|
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
|
||||||
|
|
||||||
const fetchPlan = async () => {
|
const fetchPlan = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -162,6 +165,8 @@ export const ProviderContextProvider = ({
|
|||||||
setWebappCopyrightEnabled(true)
|
setWebappCopyrightEnabled(true)
|
||||||
if (data.workspace_members)
|
if (data.workspace_members)
|
||||||
setLicenseLimit({ workspace_members: data.workspace_members })
|
setLicenseLimit({ workspace_members: data.workspace_members })
|
||||||
|
if (data.is_allow_transfer_workspace)
|
||||||
|
setIsAllowTransferWorkspace(data.is_allow_transfer_workspace)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Failed to fetch plan info:', error)
|
console.error('Failed to fetch plan info:', error)
|
||||||
@@ -222,6 +227,7 @@ export const ProviderContextProvider = ({
|
|||||||
webappCopyrightEnabled,
|
webappCopyrightEnabled,
|
||||||
licenseLimit,
|
licenseLimit,
|
||||||
refreshLicenseLimit: fetchPlan,
|
refreshLicenseLimit: fetchPlan,
|
||||||
|
isAllowTransferWorkspace,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</ProviderContext.Provider>
|
</ProviderContext.Provider>
|
||||||
|
@@ -233,6 +233,28 @@ const translation = {
|
|||||||
editWorkspaceInfo: 'Edit Workspace Info',
|
editWorkspaceInfo: 'Edit Workspace Info',
|
||||||
workspaceName: 'Workspace Name',
|
workspaceName: 'Workspace Name',
|
||||||
workspaceIcon: 'Workspace Icon',
|
workspaceIcon: 'Workspace Icon',
|
||||||
|
changeEmail: {
|
||||||
|
title: 'Change Email',
|
||||||
|
verifyEmail: 'Verify your current email',
|
||||||
|
newEmail: 'Set up a new email address',
|
||||||
|
verifyNew: 'Verify your new email',
|
||||||
|
authTip: 'Once your email is changed, Google or GitHub accounts linked to your old email will no longer be able to log in to this account.',
|
||||||
|
content1: 'If you continue, we\'ll send a verification code to <email>{{email}}</email> for re-authentication.',
|
||||||
|
content2: 'Your current email is <email>{{email}}</email>. Verification code has been sent to this email address.',
|
||||||
|
content3: 'Enter a new email and we will send you a verification code.',
|
||||||
|
content4: 'We just sent you a temporary verification code to <email>{{email}}</email>.',
|
||||||
|
codeLabel: 'Verification code',
|
||||||
|
codePlaceholder: 'Paste the 6-digit code',
|
||||||
|
emailLabel: 'New email',
|
||||||
|
emailPlaceholder: 'Enter a new email',
|
||||||
|
existingEmail: 'A user with this email already exists.',
|
||||||
|
sendVerifyCode: 'Send verification code',
|
||||||
|
continue: 'Continue',
|
||||||
|
changeTo: 'Change to {{email}}',
|
||||||
|
resendTip: 'Didn\'t receive a code?',
|
||||||
|
resendCount: 'Resend in {{count}}s',
|
||||||
|
resend: 'Resend',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
team: 'Team',
|
team: 'Team',
|
||||||
@@ -274,6 +296,26 @@ const translation = {
|
|||||||
disInvite: 'Cancel the invitation',
|
disInvite: 'Cancel the invitation',
|
||||||
deleteMember: 'Delete Member',
|
deleteMember: 'Delete Member',
|
||||||
you: '(You)',
|
you: '(You)',
|
||||||
|
transferOwnership: 'Transfer Ownership',
|
||||||
|
transferModal: {
|
||||||
|
title: 'Transfer workspace ownership',
|
||||||
|
warning: 'You\'re about to transfer ownership of “{{workspace}}”. This takes effect immediately and can\'t be undone.',
|
||||||
|
warningTip: 'You\'ll become an admin member, and the new owner will have full control.',
|
||||||
|
sendTip: 'If you continue, we\'ll send a verification code to <email>{{email}}</email> for re-authentication.',
|
||||||
|
verifyEmail: 'Verify your current email',
|
||||||
|
verifyContent: 'Your current email is <email>{{email}}</email>.',
|
||||||
|
verifyContent2: 'We\'ll send a temporary verification code to this email for re-authentication.',
|
||||||
|
codeLabel: 'Verification code',
|
||||||
|
codePlaceholder: 'Paste the 6-digit code',
|
||||||
|
resendTip: 'Didn\'t receive a code?',
|
||||||
|
resendCount: 'Resend in {{count}}s',
|
||||||
|
resend: 'Resend',
|
||||||
|
transferLabel: 'Transfer workspace ownership to',
|
||||||
|
transferPlaceholder: 'Select a workspace member…',
|
||||||
|
sendVerifyCode: 'Send verification code',
|
||||||
|
continue: 'Continue',
|
||||||
|
transfer: 'Transfer workspace ownership',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
connected: 'Connected',
|
connected: 'Connected',
|
||||||
|
@@ -234,6 +234,28 @@ const translation = {
|
|||||||
editWorkspaceInfo: 'ワークスペース情報を編集',
|
editWorkspaceInfo: 'ワークスペース情報を編集',
|
||||||
workspaceName: 'ワークスペース名',
|
workspaceName: 'ワークスペース名',
|
||||||
workspaceIcon: 'ワークスペースアイコン',
|
workspaceIcon: 'ワークスペースアイコン',
|
||||||
|
changeEmail: {
|
||||||
|
title: 'メールアドレスを変更',
|
||||||
|
verifyEmail: '現在のメールアドレスを確認してください',
|
||||||
|
newEmail: '新しいメールアドレスを設定する',
|
||||||
|
verifyNew: '新しいメールアドレスを確認してください',
|
||||||
|
authTip: 'メールアドレスが変更されると、旧メールアドレスにリンクされている Google または GitHub アカウントは、このアカウントにログインできなくなります。',
|
||||||
|
content1: '変更を続ける場合、<email>{{email}}</email> に認証用の確認コードをお送りします。',
|
||||||
|
content2: '現在のメールアドレスは <email>{{email}}</email> です。認証コードはこのメールアドレスに送信されました。',
|
||||||
|
content3: '新しいメールアドレスを入力すると、確認コードが送信されます。',
|
||||||
|
content4: '一時確認コードを <email>{{email}}</email> に送信しました。',
|
||||||
|
codeLabel: 'コード',
|
||||||
|
codePlaceholder: 'コードを入力してください',
|
||||||
|
emailLabel: '新しいメール',
|
||||||
|
emailPlaceholder: '新しいメールを入力してください',
|
||||||
|
existingEmail: 'このメールアドレスのユーザーは既に存在します',
|
||||||
|
sendVerifyCode: '確認コードを送信',
|
||||||
|
continue: '続行',
|
||||||
|
changeTo: '{{email}} に変更',
|
||||||
|
resendTip: 'コードが届きませんか?',
|
||||||
|
resendCount: '{{count}} 秒後に再送信',
|
||||||
|
resend: '再送信',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
team: 'チーム',
|
team: 'チーム',
|
||||||
@@ -275,6 +297,26 @@ const translation = {
|
|||||||
disInvite: '招待をキャンセル',
|
disInvite: '招待をキャンセル',
|
||||||
deleteMember: 'メンバーを削除',
|
deleteMember: 'メンバーを削除',
|
||||||
you: '(あなた)',
|
you: '(あなた)',
|
||||||
|
transferOwnership: '所有権の移転',
|
||||||
|
transferModal: {
|
||||||
|
title: 'ワークスペースの所有権を移する',
|
||||||
|
warning: '「{{workspace}}」の所有権を移しようとしています。この操作は即時に有効となり、元に戻すことはできません。',
|
||||||
|
warningTip: 'あなたは管理者メンバーになり、新しいオーナーがすべての権限を持つことになります。',
|
||||||
|
sendTip: '続行する場合は、本人確認のため <email>{{email}}</email> に認証コードを送信します。',
|
||||||
|
verifyEmail: '現在のメールアドレスを確認',
|
||||||
|
verifyContent: '現在のメールアドレスは <email>{{email}}</email>。',
|
||||||
|
verifyContent2: 'このメールアドレスに一時的な認証コードを送信し、再認証を行います。',
|
||||||
|
codeLabel: '認証コード',
|
||||||
|
codePlaceholder: '6 桁のコードを入力してください',
|
||||||
|
resendTip: '認証コードを受け取れない場合は、',
|
||||||
|
resendCount: '{{count}} 秒後に再送信',
|
||||||
|
resend: '認証コードを再送信',
|
||||||
|
transferLabel: 'ワークスペースの所有権を転移する相手は',
|
||||||
|
transferPlaceholder: 'メールアドレスを入力してください',
|
||||||
|
sendVerifyCode: '認証コードを送信',
|
||||||
|
continue: '続行する',
|
||||||
|
transfer: 'ワークスペースの所有権を移する',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
connected: '接続済み',
|
connected: '接続済み',
|
||||||
|
@@ -233,6 +233,28 @@ const translation = {
|
|||||||
editWorkspaceInfo: '编辑工作空间信息',
|
editWorkspaceInfo: '编辑工作空间信息',
|
||||||
workspaceName: '工作空间名称',
|
workspaceName: '工作空间名称',
|
||||||
workspaceIcon: '工作空间图标',
|
workspaceIcon: '工作空间图标',
|
||||||
|
changeEmail: {
|
||||||
|
title: '更改邮箱',
|
||||||
|
verifyEmail: '验证当前邮箱',
|
||||||
|
newEmail: '设置新邮箱',
|
||||||
|
verifyNew: '验证新邮箱',
|
||||||
|
authTip: '一旦您的电子邮件地址更改,链接到您旧电子邮件地址的 Google 或 GitHub 帐户将无法再登录该帐户。',
|
||||||
|
content1: '如果您继续,我们将向 <email>{{email}}</email> 发送验证码以进行重新验证。',
|
||||||
|
content2: '你的当前邮箱是 <email>{{email}}</email> 。验证码已发送至该邮箱。',
|
||||||
|
content3: '输入新的邮箱,我们将向您发送验证码。',
|
||||||
|
content4: '我们已将验证码发送至 <email>{{email}}</email>。',
|
||||||
|
codeLabel: '验证码',
|
||||||
|
codePlaceholder: '输入 6 位数字验证码',
|
||||||
|
emailLabel: '新邮箱',
|
||||||
|
emailPlaceholder: '输入新邮箱',
|
||||||
|
existingEmail: '该邮箱已存在',
|
||||||
|
sendVerifyCode: '发送验证码',
|
||||||
|
continue: '继续',
|
||||||
|
changeTo: '更改为 {{email}}',
|
||||||
|
resendTip: '没有收到验证码?',
|
||||||
|
resendCount: '请在 {{count}} 秒后重新发送',
|
||||||
|
resend: '重新发送',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
team: '团队',
|
team: '团队',
|
||||||
@@ -274,6 +296,26 @@ const translation = {
|
|||||||
builderTip: '可以构建和编辑自己的应用程序',
|
builderTip: '可以构建和编辑自己的应用程序',
|
||||||
setBuilder: 'Set as builder(设置为构建器)',
|
setBuilder: 'Set as builder(设置为构建器)',
|
||||||
builder: '构建器',
|
builder: '构建器',
|
||||||
|
transferOwnership: '转移所有权',
|
||||||
|
transferModal: {
|
||||||
|
title: '转移工作空间所有权',
|
||||||
|
warning: '您即将转移 “{{workspace}}”的所有权。该操作将立即生效,且无法撤销。',
|
||||||
|
warningTip: '您将成为管理员成员,新所有者将拥有完全控制权。',
|
||||||
|
sendTip: '如果您继续,我们将向 <email>{{email}}</email> 发送验证码以进行身份认证。',
|
||||||
|
verifyEmail: '验证您当前的邮箱',
|
||||||
|
verifyContent: '您当前的邮箱是 <email>{{email}}</email>。',
|
||||||
|
verifyContent2: '我们将向该邮箱发送临时验证码以完成身份验证。',
|
||||||
|
codeLabel: '验证码',
|
||||||
|
codePlaceholder: '输入 6 位数字验证码',
|
||||||
|
resendTip: '没有收到验证码?',
|
||||||
|
resendCount: '请在 {{count}} 秒后重新发送',
|
||||||
|
resend: '重新发送',
|
||||||
|
transferLabel: '新所有者',
|
||||||
|
transferPlaceholder: '选择一个成员',
|
||||||
|
sendVerifyCode: '发送验证码',
|
||||||
|
continue: '继续',
|
||||||
|
transfer: '转移工作空间所有权',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
connected: '登录方式',
|
connected: '登录方式',
|
||||||
|
@@ -131,6 +131,15 @@ export const deleteMemberOrCancelInvitation: Fetcher<CommonResponse, { url: stri
|
|||||||
return del<CommonResponse>(url)
|
return del<CommonResponse>(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendOwnerEmail = (body: { language?: string }) =>
|
||||||
|
post<CommonResponse & { data: string }>('/workspaces/current/members/send-owner-transfer-confirm-email', { body })
|
||||||
|
|
||||||
|
export const verifyOwnerEmail = (body: { code: string; token: string }) =>
|
||||||
|
post<CommonResponse & { is_valid: boolean; email: string; token: string }>('/workspaces/current/members/owner-transfer-check', { body })
|
||||||
|
|
||||||
|
export const ownershipTransfer = (memberID: string, body: { token: string }) =>
|
||||||
|
post<CommonResponse & { is_valid: boolean; email: string; token: string }>(`/workspaces/current/members/${memberID}/owner-transfer`, { body })
|
||||||
|
|
||||||
export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => {
|
export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => {
|
||||||
return get<{ content: string }>(`/files/${fileID}/preview`)
|
return get<{ content: string }>(`/files/${fileID}/preview`)
|
||||||
}
|
}
|
||||||
@@ -376,3 +385,15 @@ export const submitDeleteAccountFeedback = (body: { feedback: string; email: str
|
|||||||
|
|
||||||
export const getDocDownloadUrl = (doc_name: string) =>
|
export const getDocDownloadUrl = (doc_name: string) =>
|
||||||
get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true })
|
get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true })
|
||||||
|
|
||||||
|
export const sendVerifyCode = (body: { email: string; phase: string; token?: string }) =>
|
||||||
|
post<CommonResponse & { data: string }>('/account/change-email', { body })
|
||||||
|
|
||||||
|
export const verifyEmail = (body: { email: string; code: string; token: string }) =>
|
||||||
|
post<CommonResponse & { is_valid: boolean; email: string; token: string }>('/account/change-email/validity', { body })
|
||||||
|
|
||||||
|
export const resetEmail = (body: { new_email: string; token: string }) =>
|
||||||
|
post<CommonResponse>('/account/change-email/reset', { body })
|
||||||
|
|
||||||
|
export const checkEmailExisted = (body: { email: string }) =>
|
||||||
|
post<CommonResponse>('/account/change-email/check-email-unique', { body }, { silent: true })
|
||||||
|
@@ -35,6 +35,7 @@ export type SystemFeatures = {
|
|||||||
sso_enforced_for_web: boolean
|
sso_enforced_for_web: boolean
|
||||||
sso_enforced_for_web_protocol: SSOProtocol | ''
|
sso_enforced_for_web_protocol: SSOProtocol | ''
|
||||||
enable_marketplace: boolean
|
enable_marketplace: boolean
|
||||||
|
enable_change_email: boolean
|
||||||
enable_email_code_login: boolean
|
enable_email_code_login: boolean
|
||||||
enable_email_password_login: boolean
|
enable_email_password_login: boolean
|
||||||
enable_social_oauth_login: boolean
|
enable_social_oauth_login: boolean
|
||||||
@@ -70,6 +71,7 @@ export const defaultSystemFeatures: SystemFeatures = {
|
|||||||
sso_enforced_for_web: false,
|
sso_enforced_for_web: false,
|
||||||
sso_enforced_for_web_protocol: '',
|
sso_enforced_for_web_protocol: '',
|
||||||
enable_marketplace: false,
|
enable_marketplace: false,
|
||||||
|
enable_change_email: false,
|
||||||
enable_email_code_login: false,
|
enable_email_code_login: false,
|
||||||
enable_email_password_login: false,
|
enable_email_password_login: false,
|
||||||
enable_social_oauth_login: false,
|
enable_social_oauth_login: false,
|
||||||
|
Reference in New Issue
Block a user