Sfoglia il codice sorgente

Feat/email register refactor (#25369)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
tags/1.9.0
zyssyz123 1 mese fa
parent
commit
c2fcd2895b
Nessun account collegato all'indirizzo email del committer
36 ha cambiato i file con 2390 aggiunte e 91 eliminazioni
  1. 1
    0
      api/.env.example
  2. 11
    0
      api/configs/feature/__init__.py
  3. 1
    0
      api/controllers/console/__init__.py
  4. 155
    0
      api/controllers/console/auth/email_register.py
  5. 41
    5
      api/controllers/console/auth/error.py
  6. 13
    29
      api/controllers/console/auth/forgot_password.py
  7. 7
    21
      api/controllers/console/auth/login.py
  8. 10
    1
      api/controllers/console/auth/oauth.py
  9. 13
    0
      api/controllers/console/wraps.py
  10. 52
    0
      api/libs/email_i18n.py
  11. 112
    14
      api/services/account_service.py
  12. 87
    0
      api/tasks/mail_register_task.py
  13. 45
    0
      api/tasks/mail_reset_password_task.py
  14. 87
    0
      api/templates/register_email_template_en-US.html
  15. 87
    0
      api/templates/register_email_template_zh-CN.html
  16. 130
    0
      api/templates/register_email_when_account_exist_template_en-US.html
  17. 127
    0
      api/templates/register_email_when_account_exist_template_zh-CN.html
  18. 122
    0
      api/templates/reset_password_mail_when_account_not_exist_no_register_template_en-US.html
  19. 121
    0
      api/templates/reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html
  20. 124
    0
      api/templates/reset_password_mail_when_account_not_exist_template_en-US.html
  21. 126
    0
      api/templates/reset_password_mail_when_account_not_exist_template_zh-CN.html
  22. 83
    0
      api/templates/without-brand/register_email_template_en-US.html
  23. 83
    0
      api/templates/without-brand/register_email_template_zh-CN.html
  24. 126
    0
      api/templates/without-brand/register_email_when_account_exist_template_en-US.html
  25. 123
    0
      api/templates/without-brand/register_email_when_account_exist_template_zh-CN.html
  26. 118
    0
      api/templates/without-brand/reset_password_mail_when_account_not_exist_no_register_template_en-US.html
  27. 118
    0
      api/templates/without-brand/reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html
  28. 121
    0
      api/templates/without-brand/reset_password_mail_when_account_not_exist_template_en-US.html
  29. 120
    0
      api/templates/without-brand/reset_password_mail_when_account_not_exist_template_zh-CN.html
  30. 1
    0
      api/tests/integration_tests/.env.example
  31. 1
    2
      api/tests/test_containers_integration_tests/services/test_account_service.py
  32. 19
    15
      api/tests/unit_tests/controllers/console/auth/test_authentication_security.py
  33. 2
    2
      api/tests/unit_tests/controllers/console/auth/test_oauth.py
  34. 1
    2
      api/tests/unit_tests/services/test_account_service.py
  35. 1
    0
      docker/.env.example
  36. 1
    0
      docker/docker-compose.yaml

+ 1
- 0
api/.env.example Vedi File



# Reset password token expiry minutes # Reset password token expiry minutes
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5



+ 11
- 0
api/configs/feature/__init__.py Vedi File

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,
) )

EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="Duration in minutes for which a email register token remains valid",
default=5,
)

CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field( CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="Duration in minutes for which a change email token remains valid", description="Duration in minutes for which a change email token remains valid",
default=5, default=5,
default=86400, default=86400,
) )


EMAIL_REGISTER_LOCKOUT_DURATION: PositiveInt = Field(
description="Time (in seconds) a user must wait before retrying email register after exceeding the rate limit.",
default=86400,
)



class ModerationConfig(BaseSettings): class ModerationConfig(BaseSettings):
""" """

+ 1
- 0
api/controllers/console/__init__.py Vedi File

activate, # pyright: ignore[reportUnusedImport] activate, # pyright: ignore[reportUnusedImport]
data_source_bearer_auth, # pyright: ignore[reportUnusedImport] data_source_bearer_auth, # pyright: ignore[reportUnusedImport]
data_source_oauth, # pyright: ignore[reportUnusedImport] data_source_oauth, # pyright: ignore[reportUnusedImport]
email_register, # pyright: ignore[reportUnusedImport]
forgot_password, # pyright: ignore[reportUnusedImport] forgot_password, # pyright: ignore[reportUnusedImport]
login, # pyright: ignore[reportUnusedImport] login, # pyright: ignore[reportUnusedImport]
oauth, # pyright: ignore[reportUnusedImport] oauth, # pyright: ignore[reportUnusedImport]

+ 155
- 0
api/controllers/console/auth/email_register.py Vedi File

from flask import request
from flask_restx import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session

from configs import dify_config
from constants.languages import languages
from controllers.console import api
from controllers.console.auth.error import (
EmailAlreadyInUseError,
EmailCodeError,
EmailRegisterLimitError,
InvalidEmailError,
InvalidTokenError,
PasswordMismatchError,
)
from controllers.console.error import AccountInFreezeError, EmailSendIpLimitError
from controllers.console.wraps import email_password_login_enabled, email_register_enabled, setup_required
from extensions.ext_database import db
from libs.helper import email, extract_remote_ip
from libs.password import valid_password
from models.account import Account
from services.account_service import AccountService
from services.billing_service import BillingService
from services.errors.account import AccountNotFoundError, AccountRegisterError


class EmailRegisterSendEmailApi(Resource):
@setup_required
@email_password_login_enabled
@email_register_enabled
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("language", type=str, required=False, location="json")
args = parser.parse_args()

ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
language = "en-US"
if args["language"] in languages:
language = args["language"]

if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
raise AccountInFreezeError()

with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
token = None
token = AccountService.send_email_register_email(email=args["email"], account=account, language=language)
return {"result": "success", "data": token}


class EmailRegisterCheckApi(Resource):
@setup_required
@email_password_login_enabled
@email_register_enabled
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json")
parser.add_argument("code", type=str, required=True, location="json")
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()

user_email = args["email"]

is_email_register_error_rate_limit = AccountService.is_email_register_error_rate_limit(args["email"])
if is_email_register_error_rate_limit:
raise EmailRegisterLimitError()

token_data = AccountService.get_email_register_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_email_register_error_rate_limit(args["email"])
raise EmailCodeError()

# Verified, revoke the first token
AccountService.revoke_email_register_token(args["token"])

# Refresh token data by generating a new token
_, new_token = AccountService.generate_email_register_token(
user_email, code=args["code"], additional_data={"phase": "register"}
)

AccountService.reset_email_register_error_rate_limit(args["email"])
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}


class EmailRegisterResetApi(Resource):
@setup_required
@email_password_login_enabled
@email_register_enabled
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
parser.add_argument("new_password", type=valid_password, required=True, nullable=False, location="json")
parser.add_argument("password_confirm", type=valid_password, required=True, nullable=False, location="json")
args = parser.parse_args()

# Validate passwords match
if args["new_password"] != args["password_confirm"]:
raise PasswordMismatchError()

# Validate token and get register data
register_data = AccountService.get_email_register_data(args["token"])
if not register_data:
raise InvalidTokenError()
# Must use token in reset phase
if register_data.get("phase", "") != "register":
raise InvalidTokenError()

# Revoke token to prevent reuse
AccountService.revoke_email_register_token(args["token"])

email = register_data.get("email", "")

with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()

if account:
raise EmailAlreadyInUseError()
else:
account = self._create_new_account(email, args["password_confirm"])
if not account:
raise AccountNotFoundError()
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(email)

return {"result": "success", "data": token_pair.model_dump()}

def _create_new_account(self, email, password) -> Account | None:
# Create new account if allowed
account = None
try:
account = AccountService.create_account_and_tenant(
email=email,
name=email,
password=password,
interface_language=languages[0],
)
except AccountRegisterError:
raise AccountInFreezeError()

return account


api.add_resource(EmailRegisterSendEmailApi, "/email-register/send-email")
api.add_resource(EmailRegisterCheckApi, "/email-register/validity")
api.add_resource(EmailRegisterResetApi, "/email-register")

+ 41
- 5
api/controllers/console/auth/error.py Vedi File



class PasswordResetRateLimitExceededError(BaseHTTPException): class PasswordResetRateLimitExceededError(BaseHTTPException):
error_code = "password_reset_rate_limit_exceeded" error_code = "password_reset_rate_limit_exceeded"
description = "Too many password reset emails have been sent. Please try again in 1 minute."
description = "Too many password reset emails have been sent. Please try again in {minutes} minutes."
code = 429 code = 429


def __init__(self, minutes: int = 1):
description = self.description.format(minutes=int(minutes)) if self.description else None
super().__init__(description=description)


class EmailRegisterRateLimitExceededError(BaseHTTPException):
error_code = "email_register_rate_limit_exceeded"
description = "Too many email register emails have been sent. Please try again in {minutes} minutes."
code = 429

def __init__(self, minutes: int = 1):
description = self.description.format(minutes=int(minutes)) if self.description else None
super().__init__(description=description)



class EmailChangeRateLimitExceededError(BaseHTTPException): class EmailChangeRateLimitExceededError(BaseHTTPException):
error_code = "email_change_rate_limit_exceeded" error_code = "email_change_rate_limit_exceeded"
description = "Too many email change emails have been sent. Please try again in 1 minute."
description = "Too many email change emails have been sent. Please try again in {minutes} minutes."
code = 429 code = 429


def __init__(self, minutes: int = 1):
description = self.description.format(minutes=int(minutes)) if self.description else None
super().__init__(description=description)



class OwnerTransferRateLimitExceededError(BaseHTTPException): class OwnerTransferRateLimitExceededError(BaseHTTPException):
error_code = "owner_transfer_rate_limit_exceeded" error_code = "owner_transfer_rate_limit_exceeded"
description = "Too many owner transfer emails have been sent. Please try again in 1 minute."
description = "Too many owner transfer emails have been sent. Please try again in {minutes} minutes."
code = 429 code = 429


def __init__(self, minutes: int = 1):
description = self.description.format(minutes=int(minutes)) if self.description else None
super().__init__(description=description)



class EmailCodeError(BaseHTTPException): class EmailCodeError(BaseHTTPException):
error_code = "email_code_error" error_code = "email_code_error"


class EmailCodeLoginRateLimitExceededError(BaseHTTPException): class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
error_code = "email_code_login_rate_limit_exceeded" error_code = "email_code_login_rate_limit_exceeded"
description = "Too many login emails have been sent. Please try again in 5 minutes."
description = "Too many login emails have been sent. Please try again in {minutes} minutes."
code = 429 code = 429


def __init__(self, minutes: int = 5):
description = self.description.format(minutes=int(minutes)) if self.description else None
super().__init__(description=description)



class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException): class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
error_code = "email_code_account_deletion_rate_limit_exceeded" error_code = "email_code_account_deletion_rate_limit_exceeded"
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
description = "Too many account deletion emails have been sent. Please try again in {minutes} minutes."
code = 429 code = 429


def __init__(self, minutes: int = 5):
description = self.description.format(minutes=int(minutes)) if self.description else None
super().__init__(description=description)



class EmailPasswordResetLimitError(BaseHTTPException): class EmailPasswordResetLimitError(BaseHTTPException):
error_code = "email_password_reset_limit" error_code = "email_password_reset_limit"
code = 429 code = 429




class EmailRegisterLimitError(BaseHTTPException):
error_code = "email_register_limit"
description = "Too many failed email register attempts. Please try again in 24 hours."
code = 429


class EmailChangeLimitError(BaseHTTPException): class EmailChangeLimitError(BaseHTTPException):
error_code = "email_change_limit" error_code = "email_change_limit"
description = "Too many failed email change attempts. Please try again in 24 hours." description = "Too many failed email change attempts. Please try again in 24 hours."

+ 13
- 29
api/controllers/console/auth/forgot_password.py Vedi File

from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session


from constants.languages import languages
from controllers.console import api, console_ns from controllers.console import api, console_ns
from controllers.console.auth.error import ( from controllers.console.auth.error import (
EmailCodeError, EmailCodeError,
InvalidTokenError, InvalidTokenError,
PasswordMismatchError, PasswordMismatchError,
) )
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
from controllers.console.wraps import email_password_login_enabled, setup_required from controllers.console.wraps import email_password_login_enabled, setup_required
from events.tenant_event import tenant_was_created from events.tenant_event import tenant_was_created
from extensions.ext_database import db from extensions.ext_database import db
from libs.password import hash_password, valid_password from libs.password import hash_password, valid_password
from models.account import Account from models.account import Account
from services.account_service import AccountService, TenantService from services.account_service import AccountService, TenantService
from services.errors.account import AccountRegisterError
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
from services.feature_service import FeatureService from services.feature_service import FeatureService






with Session(db.engine) as session: with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none() account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
token = None
if account is None:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
return {"result": "fail", "data": token, "code": "account_not_found"}
else:
raise AccountNotFound()
else:
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)

token = AccountService.send_reset_password_email(
account=account,
email=args["email"],
language=language,
is_allow_register=FeatureService.get_system_features().is_allow_register,
)


return {"result": "success", "data": token} return {"result": "success", "data": token}


if account: if account:
self._update_existing_account(account, password_hashed, salt, session) self._update_existing_account(account, password_hashed, salt, session)
else: else:
self._create_new_account(email, args["password_confirm"])
raise AccountNotFound()


return {"result": "success"} return {"result": "success"}


account.current_tenant = tenant account.current_tenant = tenant
tenant_was_created.send(tenant) tenant_was_created.send(tenant)


def _create_new_account(self, email, password):
# Create new account if allowed
try:
AccountService.create_account_and_tenant(
email=email,
name=email,
password=password,
interface_language=languages[0],
)
except WorkSpaceNotAllowedCreateError:
pass
except WorkspacesLimitExceededError:
pass
except AccountRegisterError:
raise AccountInFreezeError()

api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets")

+ 7
- 21
api/controllers/console/auth/login.py Vedi File

from controllers.console.wraps import email_password_login_enabled, setup_required from controllers.console.wraps import email_password_login_enabled, setup_required
from events.tenant_event import tenant_was_created from events.tenant_event import tenant_was_created
from libs.helper import email, extract_remote_ip from libs.helper import email, extract_remote_ip
from libs.password import valid_password
from models.account import Account from models.account import Account
from services.account_service import AccountService, RegisterService, TenantService from services.account_service import AccountService, RegisterService, TenantService
from services.billing_service import BillingService from services.billing_service import BillingService
"""Authenticate user and login.""" """Authenticate user and login."""
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("password", type=valid_password, required=True, location="json")
parser.add_argument("password", type=str, required=True, location="json")
parser.add_argument("remember_me", type=bool, required=False, default=False, location="json") parser.add_argument("remember_me", type=bool, required=False, default=False, location="json")
parser.add_argument("invite_token", type=str, required=False, default=None, location="json") parser.add_argument("invite_token", type=str, required=False, default=None, location="json")
parser.add_argument("language", type=str, required=False, default="en-US", location="json")
args = parser.parse_args() args = parser.parse_args()


if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]): if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
if invitation: if invitation:
invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation) invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation)


if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"

try: try:
if invitation: if invitation:
data = invitation.get("data", {}) data = invitation.get("data", {})
except services.errors.account.AccountPasswordError: except services.errors.account.AccountPasswordError:
AccountService.add_login_error_rate_limit(args["email"]) AccountService.add_login_error_rate_limit(args["email"])
raise AuthenticationFailedError() raise AuthenticationFailedError()
except services.errors.account.AccountNotFoundError:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
return {"result": "fail", "data": token, "code": "account_not_found"}
else:
raise AccountNotFound()
# SELF_HOSTED only have one workspace # SELF_HOSTED only have one workspace
tenants = TenantService.get_join_tenants(account) tenants = TenantService.get_join_tenants(account)
if len(tenants) == 0: if len(tenants) == 0:
except AccountRegisterError: except AccountRegisterError:
raise AccountInFreezeError() raise AccountInFreezeError()


if account is None:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
else:
raise AccountNotFound()
else:
token = AccountService.send_reset_password_email(account=account, language=language)
token = AccountService.send_reset_password_email(
email=args["email"],
account=account,
language=language,
is_allow_register=FeatureService.get_system_features().is_allow_register,
)


return {"result": "success", "data": token} return {"result": "success", "data": token}



+ 10
- 1
api/controllers/console/auth/oauth.py Vedi File

from models import Account from models import Account
from models.account import AccountStatus from models.account import AccountStatus
from services.account_service import AccountService, RegisterService, TenantService from services.account_service import AccountService, RegisterService, TenantService
from services.billing_service import BillingService
from services.errors.account import AccountNotFoundError, AccountRegisterError from services.errors.account import AccountNotFoundError, AccountRegisterError
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
from services.feature_service import FeatureService from services.feature_service import FeatureService


if not account: if not account:
if not FeatureService.get_system_features().is_allow_register: if not FeatureService.get_system_features().is_allow_register:
raise AccountNotFoundError()
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(user_info.email):
raise AccountRegisterError(
description=(
"This email account has been deleted within the past "
"30 days and is temporarily unavailable for new account registration"
)
)
else:
raise AccountRegisterError(description=("Invalid email or password"))
account_name = user_info.name or "Dify" account_name = user_info.name or "Dify"
account = RegisterService.register( account = RegisterService.register(
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider

+ 13
- 0
api/controllers/console/wraps.py Vedi File

return decorated return decorated




def email_register_enabled(view):
@wraps(view)
def decorated(*args, **kwargs):
features = FeatureService.get_system_features()
if features.is_allow_register:
return view(*args, **kwargs)

# otherwise, return 403
abort(403)

return decorated


def enable_change_email(view: Callable[P, R]): def enable_change_email(view: Callable[P, R]):
@wraps(view) @wraps(view)
def decorated(*args: P.args, **kwargs: P.kwargs): def decorated(*args: P.args, **kwargs: P.kwargs):

+ 52
- 0
api/libs/email_i18n.py Vedi File

"""Enumeration of supported email types.""" """Enumeration of supported email types."""


RESET_PASSWORD = "reset_password" RESET_PASSWORD = "reset_password"
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST = "reset_password_when_account_not_exist"
INVITE_MEMBER = "invite_member" INVITE_MEMBER = "invite_member"
EMAIL_CODE_LOGIN = "email_code_login" EMAIL_CODE_LOGIN = "email_code_login"
CHANGE_EMAIL_OLD = "change_email_old" CHANGE_EMAIL_OLD = "change_email_old"
ENTERPRISE_CUSTOM = "enterprise_custom" ENTERPRISE_CUSTOM = "enterprise_custom"
QUEUE_MONITOR_ALERT = "queue_monitor_alert" QUEUE_MONITOR_ALERT = "queue_monitor_alert"
DOCUMENT_CLEAN_NOTIFY = "document_clean_notify" DOCUMENT_CLEAN_NOTIFY = "document_clean_notify"
EMAIL_REGISTER = "email_register"
EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = "email_register_when_account_exist"
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = "reset_password_when_account_not_exist_no_register"




class EmailLanguage(Enum): class EmailLanguage(Enum):
branded_template_path="clean_document_job_mail_template_zh-CN.html", branded_template_path="clean_document_job_mail_template_zh-CN.html",
), ),
}, },
EmailType.EMAIL_REGISTER: {
EmailLanguage.EN_US: EmailTemplate(
subject="Register Your {application_title} Account",
template_path="register_email_template_en-US.html",
branded_template_path="without-brand/register_email_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="注册您的 {application_title} 账户",
template_path="register_email_template_zh-CN.html",
branded_template_path="without-brand/register_email_template_zh-CN.html",
),
},
EmailType.EMAIL_REGISTER_WHEN_ACCOUNT_EXIST: {
EmailLanguage.EN_US: EmailTemplate(
subject="Register Your {application_title} Account",
template_path="register_email_when_account_exist_template_en-US.html",
branded_template_path="without-brand/register_email_when_account_exist_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="注册您的 {application_title} 账户",
template_path="register_email_when_account_exist_template_zh-CN.html",
branded_template_path="without-brand/register_email_when_account_exist_template_zh-CN.html",
),
},
EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST: {
EmailLanguage.EN_US: EmailTemplate(
subject="Reset Your {application_title} Password",
template_path="reset_password_mail_when_account_not_exist_template_en-US.html",
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="重置您的 {application_title} 密码",
template_path="reset_password_mail_when_account_not_exist_template_zh-CN.html",
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_template_zh-CN.html",
),
},
EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER: {
EmailLanguage.EN_US: EmailTemplate(
subject="Reset Your {application_title} Password",
template_path="reset_password_mail_when_account_not_exist_no_register_template_en-US.html",
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_no_register_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="重置您的 {application_title} 密码",
template_path="reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html",
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html",
),
},
} }


return EmailI18nConfig(templates=templates) return EmailI18nConfig(templates=templates)

+ 112
- 14
api/services/account_service.py Vedi File

from services.errors.account import ( from services.errors.account import (
AccountAlreadyInTenantError, AccountAlreadyInTenantError,
AccountLoginError, AccountLoginError,
AccountNotFoundError,
AccountNotLinkTenantError, AccountNotLinkTenantError,
AccountPasswordError, AccountPasswordError,
AccountRegisterError, AccountRegisterError,
send_old_owner_transfer_notify_email_task, send_old_owner_transfer_notify_email_task,
send_owner_transfer_confirm_task, send_owner_transfer_confirm_task,
) )
from tasks.mail_reset_password_task import send_reset_password_mail_task
from tasks.mail_register_task import send_email_register_mail_task, send_email_register_mail_task_when_account_exist
from tasks.mail_reset_password_task import (
send_reset_password_mail_task,
send_reset_password_mail_task_when_account_not_exist,
)


logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)




class AccountService: class AccountService:
reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=1, time_window=60 * 1) reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=1, time_window=60 * 1)
email_register_rate_limiter = RateLimiter(prefix="email_register_rate_limit", max_attempts=1, time_window=60 * 1)
email_code_login_rate_limiter = RateLimiter( email_code_login_rate_limiter = RateLimiter(
prefix="email_code_login_rate_limit", max_attempts=1, time_window=60 * 1
prefix="email_code_login_rate_limit", max_attempts=3, time_window=300 * 1
) )
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
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5 FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
CHANGE_EMAIL_MAX_ERROR_LIMITS = 5 CHANGE_EMAIL_MAX_ERROR_LIMITS = 5
OWNER_TRANSFER_MAX_ERROR_LIMITS = 5 OWNER_TRANSFER_MAX_ERROR_LIMITS = 5
EMAIL_REGISTER_MAX_ERROR_LIMITS = 5


@staticmethod @staticmethod
def _get_refresh_token_key(refresh_token: str) -> str: def _get_refresh_token_key(refresh_token: str) -> str:


account = db.session.query(Account).filter_by(email=email).first() account = db.session.query(Account).filter_by(email=email).first()
if not account: if not account:
raise AccountNotFoundError()
raise AccountPasswordError("Invalid email or password.")


if account.status == AccountStatus.BANNED.value: if account.status == AccountStatus.BANNED.value:
raise AccountLoginError("Account is banned.") raise AccountLoginError("Account is banned.")
if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email):
from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError


raise EmailCodeAccountDeletionRateLimitExceededError()
raise EmailCodeAccountDeletionRateLimitExceededError(
int(cls.email_code_account_deletion_rate_limiter.time_window / 60)
)


send_account_deletion_verification_code.delay(to=email, code=code) send_account_deletion_verification_code.delay(to=email, code=code)


account: Optional[Account] = None, account: Optional[Account] = None,
email: Optional[str] = None, email: Optional[str] = None,
language: str = "en-US", language: str = "en-US",
is_allow_register: bool = False,
): ):
account_email = account.email if account else email account_email = account.email if account else email
if account_email is None: if account_email is None:
if cls.reset_password_rate_limiter.is_rate_limited(account_email): if cls.reset_password_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import PasswordResetRateLimitExceededError from controllers.console.auth.error import PasswordResetRateLimitExceededError


raise PasswordResetRateLimitExceededError()
raise PasswordResetRateLimitExceededError(int(cls.reset_password_rate_limiter.time_window / 60))


code, token = cls.generate_reset_password_token(account_email, account) code, token = cls.generate_reset_password_token(account_email, account)


send_reset_password_mail_task.delay(
language=language,
to=account_email,
code=code,
)
if account:
send_reset_password_mail_task.delay(
language=language,
to=account_email,
code=code,
)
else:
send_reset_password_mail_task_when_account_not_exist.delay(
language=language,
to=account_email,
is_allow_register=is_allow_register,
)
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_email_register_email(
cls,
account: Optional[Account] = None,
email: Optional[str] = None,
language: str = "en-US",
):
account_email = account.email if account else email
if account_email is None:
raise ValueError("Email must be provided.")

if cls.email_register_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import EmailRegisterRateLimitExceededError

raise EmailRegisterRateLimitExceededError(int(cls.email_register_rate_limiter.time_window / 60))

code, token = cls.generate_email_register_token(account_email)

if account:
send_email_register_mail_task_when_account_exist.delay(
language=language,
to=account_email,
account_name=account.name,
)

else:
send_email_register_mail_task.delay(
language=language,
to=account_email,
code=code,
)
cls.email_register_rate_limiter.increment_rate_limit(account_email)
return token

@classmethod @classmethod
def send_change_email_email( def send_change_email_email(
cls, cls,
if cls.change_email_rate_limiter.is_rate_limited(account_email): if cls.change_email_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import EmailChangeRateLimitExceededError from controllers.console.auth.error import EmailChangeRateLimitExceededError


raise EmailChangeRateLimitExceededError()
raise EmailChangeRateLimitExceededError(int(cls.change_email_rate_limiter.time_window / 60))


code, token = cls.generate_change_email_token(account_email, account, old_email=old_email) code, token = cls.generate_change_email_token(account_email, account, old_email=old_email)


if cls.owner_transfer_rate_limiter.is_rate_limited(account_email): if cls.owner_transfer_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import OwnerTransferRateLimitExceededError from controllers.console.auth.error import OwnerTransferRateLimitExceededError


raise OwnerTransferRateLimitExceededError()
raise OwnerTransferRateLimitExceededError(int(cls.owner_transfer_rate_limiter.time_window / 60))


code, token = cls.generate_owner_transfer_token(account_email, account) code, token = cls.generate_owner_transfer_token(account_email, account)
workspace_name = workspace_name or "" workspace_name = workspace_name or ""
) )
return code, token return code, token


@classmethod
def generate_email_register_token(
cls,
email: str,
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(email=email, token_type="email_register", additional_data=additional_data)
return code, token

@classmethod @classmethod
def generate_change_email_token( def generate_change_email_token(
cls, cls,
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_email_register_token(cls, token: str):
TokenManager.revoke_token(token, "email_register")

@classmethod @classmethod
def revoke_change_email_token(cls, token: str): def revoke_change_email_token(cls, token: str):
TokenManager.revoke_token(token, "change_email") TokenManager.revoke_token(token, "change_email")
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_email_register_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "email_register")

@classmethod @classmethod
def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]: def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "change_email") return TokenManager.get_token_data(token, "change_email")
if cls.email_code_login_rate_limiter.is_rate_limited(email): if cls.email_code_login_rate_limiter.is_rate_limited(email):
from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError


raise EmailCodeLoginRateLimitExceededError()
raise EmailCodeLoginRateLimitExceededError(int(cls.email_code_login_rate_limiter.time_window / 60))


code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
token = TokenManager.generate_token( token = TokenManager.generate_token(
count = int(count) + 1 count = int(count) + 1
redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count) redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count)


@staticmethod
@redis_fallback(default_return=None)
def add_email_register_error_rate_limit(email: str) -> None:
key = f"email_register_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.EMAIL_REGISTER_LOCKOUT_DURATION, count)

@staticmethod @staticmethod
@redis_fallback(default_return=False) @redis_fallback(default_return=False)
def is_forgot_password_error_rate_limit(email: str) -> bool: def is_forgot_password_error_rate_limit(email: str) -> bool:
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=False)
def is_email_register_error_rate_limit(email: str) -> bool:
key = f"email_register_error_rate_limit:{email}"
count = redis_client.get(key)
if count is None:
return False
count = int(count)
if count > AccountService.EMAIL_REGISTER_MAX_ERROR_LIMITS:
return True
return False

@staticmethod
@redis_fallback(default_return=None)
def reset_email_register_error_rate_limit(email: str):
key = f"email_register_error_rate_limit:{email}"
redis_client.delete(key)

@staticmethod @staticmethod
@redis_fallback(default_return=None) @redis_fallback(default_return=None)
def add_change_email_error_rate_limit(email: str): def add_change_email_error_rate_limit(email: str):

+ 87
- 0
api/tasks/mail_register_task.py Vedi File

import logging
import time

import click
from celery import shared_task

from configs import dify_config
from extensions.ext_mail import mail
from libs.email_i18n import EmailType, get_email_i18n_service

logger = logging.getLogger(__name__)


@shared_task(queue="mail")
def send_email_register_mail_task(language: str, to: str, code: str) -> None:
"""
Send email register email with internationalization support.

Args:
language: Language code for email localization
to: Recipient email address
code: Email register code
"""
if not mail.is_inited():
return

logger.info(click.style(f"Start email register mail to {to}", fg="green"))
start_at = time.perf_counter()

try:
email_service = get_email_i18n_service()
email_service.send_email(
email_type=EmailType.EMAIL_REGISTER,
language_code=language,
to=to,
template_context={
"to": to,
"code": code,
},
)

end_at = time.perf_counter()
logger.info(
click.style(f"Send email register mail to {to} succeeded: latency: {end_at - start_at}", fg="green")
)
except Exception:
logger.exception("Send email register mail to %s failed", to)


@shared_task(queue="mail")
def send_email_register_mail_task_when_account_exist(language: str, to: str, account_name: str) -> None:
"""
Send email register email with internationalization support when account exist.

Args:
language: Language code for email localization
to: Recipient email address
"""
if not mail.is_inited():
return

logger.info(click.style(f"Start email register mail to {to}", fg="green"))
start_at = time.perf_counter()

try:
login_url = f"{dify_config.CONSOLE_WEB_URL}/signin"
reset_password_url = f"{dify_config.CONSOLE_WEB_URL}/reset-password"

email_service = get_email_i18n_service()
email_service.send_email(
email_type=EmailType.EMAIL_REGISTER_WHEN_ACCOUNT_EXIST,
language_code=language,
to=to,
template_context={
"to": to,
"login_url": login_url,
"reset_password_url": reset_password_url,
"account_name": account_name,
},
)

end_at = time.perf_counter()
logger.info(
click.style(f"Send email register mail to {to} succeeded: latency: {end_at - start_at}", fg="green")
)
except Exception:
logger.exception("Send email register mail to %s failed", to)

+ 45
- 0
api/tasks/mail_reset_password_task.py Vedi File

import click import click
from celery import shared_task from celery import shared_task


from configs import dify_config
from extensions.ext_mail import mail from extensions.ext_mail import mail
from libs.email_i18n import EmailType, get_email_i18n_service from libs.email_i18n import EmailType, get_email_i18n_service


) )
except Exception: except Exception:
logger.exception("Send password reset mail to %s failed", to) logger.exception("Send password reset mail to %s failed", to)


@shared_task(queue="mail")
def send_reset_password_mail_task_when_account_not_exist(language: str, to: str, is_allow_register: bool) -> None:
"""
Send reset password email with internationalization support when account not exist.

Args:
language: Language code for email localization
to: Recipient email address
"""
if not mail.is_inited():
return

logger.info(click.style(f"Start password reset mail to {to}", fg="green"))
start_at = time.perf_counter()

try:
if is_allow_register:
sign_up_url = f"{dify_config.CONSOLE_WEB_URL}/signup"
email_service = get_email_i18n_service()
email_service.send_email(
email_type=EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST,
language_code=language,
to=to,
template_context={
"to": to,
"sign_up_url": sign_up_url,
},
)
else:
email_service = get_email_i18n_service()
email_service.send_email(
email_type=EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER,
language_code=language,
to=to,
)

end_at = time.perf_counter()
logger.info(
click.style(f"Send password reset mail to {to} succeeded: latency: {end_at - start_at}", fg="green")
)
except Exception:
logger.exception("Send password reset mail to %s failed", to)

+ 87
- 0
api/templates/register_email_template_en-US.html Vedi File

<!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: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}

.header {
margin-bottom: 24px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}

.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</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">Dify Sign-up Code</p>
<p class="description">Your sign-up code for Dify

Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didn't request this code, don't worry. You can safely ignore this email.</p>
</div>
</body>

</html>

+ 87
- 0
api/templates/register_email_template_zh-CN.html Vedi File

<!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: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}

.header {
margin-bottom: 24px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}

.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</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">Dify 注册验证码</p>
<p class="description">您的 Dify 注册验证码

复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求,请不要担心。您可以安全地忽略此电子邮件。</p>
</div>
</body>

</html>

+ 130
- 0
api/templates/register_email_when_account_exist_template_en-US.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 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">It looks like you’re signing up with an existing account</p>
<p class="description">Hi, {{account_name}}</p>
<p class="description">
We noticed you tried to sign up, but this email is already registered with an existing account.

Please log in here: </p>
<a href="{{ login_url }}" class="button">Log In</a>
<p class="description">
If you forgot your password, you can reset it here: <a href="{{ reset_password_url }}"
class="reset-btn">Reset Password</a>
</p>
<p class="description">
If you didn’t request this action, you can safely ignore this email.
</p>
</div>
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
</body>

</html>

+ 127
- 0
api/templates/register_email_when_account_exist_template_zh-CN.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 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">您似乎正在使用现有账户注册</p>
<p class="description">您好,{{account_name}}</p>
<p class="description">
我们注意到您尝试注册,但此电子邮件已注册。

请在此登录: </p>
<a href="{{ login_url }}" class="button">登录</a>
<p class="description">
如果您忘记了密码,可以在此重置: <a href="{{ reset_password_url }}" class="reset-btn">重置密码</a>
</p>
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
</div>
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
</body>

</html>

+ 122
- 0
api/templates/reset_password_mail_when_account_not_exist_no_register_template_en-US.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 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">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi, </p>
<p class="description">
We noticed you tried to reset your password, but this email is not associated with any account.
</p>
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
</div>
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
</body>

</html>

+ 121
- 0
api/templates/reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 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">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">您好,</p>
<p class="description">
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。</p>
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
</div>
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
</body>

</html>

+ 124
- 0
api/templates/reset_password_mail_when_account_not_exist_template_en-US.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 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">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi, </p>
<p class="description">
We noticed you tried to reset your password, but this email is not associated with any account.

Please sign up here: </p>
<a href="{{ sign_up_url }}" class="button">Sign Up</a>
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
</div>
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
</body>

</html>

+ 126
- 0
api/templates/reset_password_mail_when_account_not_exist_template_zh-CN.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 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">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">您好, </p>
<p class="description">
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。

请在此注册: </p>
<p class="description">
<a href="{{ sign_up_url }}" class="button">注册</a>
</p>
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
</div>
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
</body>

</html>

+ 83
- 0
api/templates/without-brand/register_email_template_en-US.html Vedi File

<!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: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}

.header {
margin-bottom: 24px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}

.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">{{application_title}} Sign-up Code</p>
<p class="description">Your sign-up code

Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didn't request this code, don't worry. You can safely ignore this email.</p>
</div>
</body>

</html>

+ 83
- 0
api/templates/without-brand/register_email_template_zh-CN.html Vedi File

<!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: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}

.header {
margin-bottom: 24px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}

.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">{{application_title}} 注册验证码</p>
<p class="description">您的 {{application_title}} 注册验证码

复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求此验证码,请不要担心。您可以安全地忽略此电子邮件。</p>
</div>
</body>

</html>

+ 126
- 0
api/templates/without-brand/register_email_when_account_exist_template_en-US.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">It looks like you’re signing up with an existing account</p>
<p class="description">Hi, {{account_name}}</p>
<p class="description">
We noticed you tried to sign up, but this email is already registered with an existing account.

Please log in here: </p>
<a href="{{ login_url }}" class="button">Log In</a>
<p class="description">
If you forgot your password, you can reset it here: <a href="{{ reset_password_url }}"
class="reset-btn">Reset Password</a>
</p>
<p class="description">
If you didn’t request this action, you can safely ignore this email.
</p>
</div>
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
</body>

</html>

+ 123
- 0
api/templates/without-brand/register_email_when_account_exist_template_zh-CN.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">您似乎正在使用现有账户注册</p>
<p class="description">您好,{{account_name}}</p>
<p class="description">
我们注意到您尝试注册,但此电子邮件已注册。

请在此登录: </p>
<a href="{{ login_url }}" class="button">登录</a>
<p class="description">
如果您忘记了密码,可以在此重置: <a href="{{ reset_password_url }}" class="reset-btn">重置密码</a>
</p>
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
</div>
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
</body>

</html>

+ 118
- 0
api/templates/without-brand/reset_password_mail_when_account_not_exist_no_register_template_en-US.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi,</p>
<p class="description">
We noticed you tried to reset your password, but this email is not associated with any account.
</p>
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
</div>
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>s
</body>

</html>

+ 118
- 0
api/templates/without-brand/reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">您好,</p>
<p class="description">
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
</p>
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
</div>
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
</body>

</html>

+ 121
- 0
api/templates/without-brand/reset_password_mail_when_account_not_exist_template_en-US.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi,</p>
<p class="description">
We noticed you tried to reset your password, but this email is not associated with any account.

Please sign up here: </p>
<a href="{{ sign_up_url }}" class="button">Sign Up</a>
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>

</div>
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
</body>

</html>

+ 120
- 0
api/templates/without-brand/reset_password_mail_when_account_not_exist_template_zh-CN.html Vedi File

<!DOCTYPE html>
<html>

<head>
<style>
body {
font-family: 'Arial', sans-serif;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}

.container {
width: 600px;
margin: 80px auto 0 auto;
padding: 36px 48px 52px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
}

.header {
margin-bottom: 40px;
}

.header img {
max-width: 100px;
height: auto;
}

.title {
margin-bottom: 32px;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
color: #101828;
}

.description {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
line-height: 20px;
color: #676f83;
}

.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}

.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}

.button {
display: block;
background: #2563eb;
color: #fff !important;
text-decoration: none;
font-weight: 600;
border-radius: 10px;
border: 0.5px solid rgba(16, 24, 40, 0.04);
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
margin-top: 12px;
margin-bottom: 20px;
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
}

.reset-btn {
color: #155AEF;
text-decoration: none;
font-weight: 500;
}

.support {
color: #155AEF;
text-decoration: none;
}

.support:hover {
text-decoration: underline;
}

.tip {
margin-top: 20px;
color: #676F83;
text-align: center;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
</style>
</head>

<body>
<div class="container">
<p class="title">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">您好, </p>
<p class="description">
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。

请在此注册: </p>
<a href="{{ sign_up_url }}" class="button">注册</a>
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
</div>
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
</body>

</html>

+ 1
- 0
api/tests/integration_tests/.env.example Vedi File



# Reset password token expiry minutes # Reset password token expiry minutes
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5



+ 1
- 2
api/tests/test_containers_integration_tests/services/test_account_service.py Vedi File

from services.errors.account import ( from services.errors.account import (
AccountAlreadyInTenantError, AccountAlreadyInTenantError,
AccountLoginError, AccountLoginError,
AccountNotFoundError,
AccountPasswordError, AccountPasswordError,
AccountRegisterError, AccountRegisterError,
CurrentPasswordIncorrectError, CurrentPasswordIncorrectError,
fake = Faker() fake = Faker()
email = fake.email() email = fake.email()
password = fake.password(length=12) password = fake.password(length=12)
with pytest.raises(AccountNotFoundError):
with pytest.raises(AccountPasswordError):
AccountService.authenticate(email, password) AccountService.authenticate(email, password)


def test_authenticate_banned_account(self, db_session_with_containers, mock_external_service_dependencies): def test_authenticate_banned_account(self, db_session_with_containers, mock_external_service_dependencies):

+ 19
- 15
api/tests/unit_tests/controllers/console/auth/test_authentication_security.py Vedi File

import services.errors.account import services.errors.account
from controllers.console.auth.error import AuthenticationFailedError from controllers.console.auth.error import AuthenticationFailedError
from controllers.console.auth.login import LoginApi from controllers.console.auth.login import LoginApi
from controllers.console.error import AccountNotFound




class TestAuthenticationSecurity: class TestAuthenticationSecurity:
@patch("controllers.console.auth.login.FeatureService.get_system_features") @patch("controllers.console.auth.login.FeatureService.get_system_features")
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit") @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.AccountService.authenticate") @patch("controllers.console.auth.login.AccountService.authenticate")
@patch("controllers.console.auth.login.AccountService.send_reset_password_email")
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False) @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
@patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid") @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
def test_login_invalid_email_with_registration_allowed( def test_login_invalid_email_with_registration_allowed(
self, mock_get_invitation, mock_send_email, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
): ):
"""Test that invalid email sends reset password email when registration is allowed."""
"""Test that invalid email raises AuthenticationFailedError when account not found."""
# Arrange # Arrange
mock_is_rate_limit.return_value = False mock_is_rate_limit.return_value = False
mock_get_invitation.return_value = None mock_get_invitation.return_value = None
mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
mock_authenticate.side_effect = services.errors.account.AccountPasswordError("Invalid email or password.")
mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
mock_features.return_value.is_allow_register = True mock_features.return_value.is_allow_register = True
mock_send_email.return_value = "token123"


# Act # Act
with self.app.test_request_context( with self.app.test_request_context(
"/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"} "/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
): ):
login_api = LoginApi() login_api = LoginApi()
result = login_api.post()


# Assert
assert result == {"result": "fail", "data": "token123", "code": "account_not_found"}
mock_send_email.assert_called_once_with(email="nonexistent@example.com", language="en-US")
# Assert
with pytest.raises(AuthenticationFailedError) as exc_info:
login_api.post()

assert exc_info.value.error_code == "authentication_failed"
assert exc_info.value.description == "Invalid email or password."
mock_add_rate_limit.assert_called_once_with("nonexistent@example.com")


@patch("controllers.console.wraps.db") @patch("controllers.console.wraps.db")
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit") @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.FeatureService.get_system_features") @patch("controllers.console.auth.login.FeatureService.get_system_features")
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit") @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
@patch("controllers.console.auth.login.AccountService.authenticate") @patch("controllers.console.auth.login.AccountService.authenticate")
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False) @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
@patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid") @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
def test_login_invalid_email_with_registration_disabled( def test_login_invalid_email_with_registration_disabled(
self, mock_get_invitation, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
): ):
"""Test that invalid email raises AccountNotFound when registration is disabled."""
"""Test that invalid email raises AuthenticationFailedError when account not found."""
# Arrange # Arrange
mock_is_rate_limit.return_value = False mock_is_rate_limit.return_value = False
mock_get_invitation.return_value = None mock_get_invitation.return_value = None
mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
mock_authenticate.side_effect = services.errors.account.AccountPasswordError("Invalid email or password.")
mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
mock_features.return_value.is_allow_register = False mock_features.return_value.is_allow_register = False


login_api = LoginApi() login_api = LoginApi()


# Assert # Assert
with pytest.raises(AccountNotFound) as exc_info:
with pytest.raises(AuthenticationFailedError) as exc_info:
login_api.post() login_api.post()


assert exc_info.value.error_code == "account_not_found"
assert exc_info.value.error_code == "authentication_failed"
assert exc_info.value.description == "Invalid email or password."
mock_add_rate_limit.assert_called_once_with("nonexistent@example.com")


@patch("controllers.console.wraps.db") @patch("controllers.console.wraps.db")
@patch("controllers.console.auth.login.FeatureService.get_system_features") @patch("controllers.console.auth.login.FeatureService.get_system_features")

+ 2
- 2
api/tests/unit_tests/controllers/console/auth/test_oauth.py Vedi File

) )
from libs.oauth import OAuthUserInfo from libs.oauth import OAuthUserInfo
from models.account import AccountStatus from models.account import AccountStatus
from services.errors.account import AccountNotFoundError
from services.errors.account import AccountRegisterError




class TestGetOAuthProviders: class TestGetOAuthProviders:


with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}): with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}):
if not allow_register and not existing_account: if not allow_register and not existing_account:
with pytest.raises(AccountNotFoundError):
with pytest.raises(AccountRegisterError):
_generate_account("github", user_info) _generate_account("github", user_info)
else: else:
result = _generate_account("github", user_info) result = _generate_account("github", user_info)

+ 1
- 2
api/tests/unit_tests/services/test_account_service.py Vedi File

from services.errors.account import ( from services.errors.account import (
AccountAlreadyInTenantError, AccountAlreadyInTenantError,
AccountLoginError, AccountLoginError,
AccountNotFoundError,
AccountPasswordError, AccountPasswordError,
AccountRegisterError, AccountRegisterError,
CurrentPasswordIncorrectError, CurrentPasswordIncorrectError,


# Execute test and verify exception # Execute test and verify exception
self._assert_exception_raised( self._assert_exception_raised(
AccountNotFoundError, AccountService.authenticate, "notfound@example.com", "password"
AccountPasswordError, AccountService.authenticate, "notfound@example.com", "password"
) )


def test_authenticate_account_banned(self, mock_db_dependencies): def test_authenticate_account_banned(self, mock_db_dependencies):

+ 1
- 0
docker/.env.example Vedi File



# Reset password token valid time (minutes), # Reset password token valid time (minutes),
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5



+ 1
- 0
docker/docker-compose.yaml Vedi File

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}
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES: ${EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES:-5}
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: ${CHANGE_EMAIL_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} 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}

Loading…
Annulla
Salva