Revert "feat: email register refactor" (#25367)

This commit is contained in:
zyssyz123 2025-09-08 19:20:09 +08:00 • committed by GitHub
commit ea61420441
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 79 additions and 1916 deletions

View file

@ -530,7 +530,6 @@ 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
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

View file

@ -31,12 +31,6 @@ 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,
) )
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,
@ -645,11 +639,6 @@ class AuthConfig(BaseSettings):
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):
""" """

View file

@ -70,16 +70,7 @@ from .app import (
) )
# Import auth controllers # Import auth controllers
from .auth import ( from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth, oauth_server
activate,
data_source_bearer_auth,
data_source_oauth,
email_register,
forgot_password,
login,
oauth,
oauth_server,
)
# Import billing controllers # Import billing controllers
from .billing import billing, compliance from .billing import billing, compliance

View file

@ -1,154 +0,0 @@
from flask import request
from flask_restx import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
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.errors.account import AccountRegisterError
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
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()
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
token = None
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"])
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):
# Create new account if allowed
try:
account = 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()
return account
api.add_resource(EmailRegisterSendEmailApi, "/email-register/send-email")
api.add_resource(EmailRegisterCheckApi, "/email-register/validity")
api.add_resource(EmailRegisterResetApi, "/email-register")

View file

@ -31,12 +31,6 @@ class PasswordResetRateLimitExceededError(BaseHTTPException):
code = 429 code = 429
class EmailRegisterRateLimitExceededError(BaseHTTPException):
error_code = "email_register_rate_limit_exceeded"
description = "Too many email register emails have been sent. Please try again in 1 minute."
code = 429
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 1 minute."
@ -91,12 +85,6 @@ class EmailPasswordResetLimitError(BaseHTTPException):
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."

View file

@ -6,6 +6,7 @@ from flask_restx import Resource, reqparse
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 from controllers.console import api
from controllers.console.auth.error import ( from controllers.console.auth.error import (
EmailCodeError, EmailCodeError,
@ -14,7 +15,7 @@ from controllers.console.auth.error import (
InvalidTokenError, InvalidTokenError,
PasswordMismatchError, PasswordMismatchError,
) )
from controllers.console.error import AccountNotFound, EmailSendIpLimitError from controllers.console.error import AccountInFreezeError, 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
@ -22,6 +23,8 @@ from libs.helper import email, extract_remote_ip
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
@ -45,13 +48,15 @@ class ForgotPasswordSendEmailApi(Resource):
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
token = AccountService.send_reset_password_email( if account is None:
account=account, if FeatureService.get_system_features().is_allow_register:
email=args["email"], token = AccountService.send_reset_password_email(email=args["email"], language=language)
language=language, return {"result": "fail", "data": token, "code": "account_not_found"}
is_allow_register=FeatureService.get_system_features().is_allow_register, else:
) raise AccountNotFound()
else:
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
return {"result": "success", "data": token} return {"result": "success", "data": token}
@ -132,7 +137,7 @@ class ForgotPasswordResetApi(Resource):
if account: if account:
self._update_existing_account(account, password_hashed, salt, session) self._update_existing_account(account, password_hashed, salt, session)
else: else:
raise AccountNotFound() self._create_new_account(email, args["password_confirm"])
return {"result": "success"} return {"result": "success"}
@ -152,6 +157,22 @@ class ForgotPasswordResetApi(Resource):
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(ForgotPasswordSendEmailApi, "/forgot-password")
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity") api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")

View file

@ -26,6 +26,7 @@ from controllers.console.error import (
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
@ -43,9 +44,10 @@ class LoginApi(Resource):
"""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=str, required=True, location="json") parser.add_argument("password", type=valid_password, 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"]):
@ -59,6 +61,11 @@ class LoginApi(Resource):
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", {})
@ -73,6 +80,12 @@ class LoginApi(Resource):
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:
@ -120,12 +133,13 @@ class ResetPasswordSendEmailApi(Resource):
except AccountRegisterError: except AccountRegisterError:
raise AccountInFreezeError() raise AccountInFreezeError()
token = AccountService.send_reset_password_email( if account is None:
email=args["email"], if FeatureService.get_system_features().is_allow_register:
account=account, token = AccountService.send_reset_password_email(email=args["email"], language=language)
language=language, else:
is_allow_register=FeatureService.get_system_features().is_allow_register, raise AccountNotFound()
) else:
token = AccountService.send_reset_password_email(account=account, language=language)
return {"result": "success", "data": token} return {"result": "success", "data": token}

View file

@ -242,19 +242,6 @@ def email_password_login_enabled(view: Callable[P, R]):
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):

View file

@ -21,7 +21,6 @@ class EmailType(Enum):
"""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"
@ -35,9 +34,6 @@ class EmailType(Enum):
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):
@ -445,54 +441,6 @@ def create_default_email_config() -> EmailI18nConfig:
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)

View file

@ -37,6 +37,7 @@ from services.billing_service import BillingService
from services.errors.account import ( from services.errors.account import (
AccountAlreadyInTenantError, AccountAlreadyInTenantError,
AccountLoginError, AccountLoginError,
AccountNotFoundError,
AccountNotLinkTenantError, AccountNotLinkTenantError,
AccountPasswordError, AccountPasswordError,
AccountRegisterError, AccountRegisterError,
@ -64,11 +65,7 @@ from tasks.mail_owner_transfer_task import (
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_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
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__)
@ -85,7 +82,6 @@ REFRESH_TOKEN_EXPIRY = timedelta(days=dify_config.REFRESH_TOKEN_EXPIRE_DAYS)
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=1, time_window=60 * 1
) )
@ -99,7 +95,6 @@ class AccountService:
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:
@ -176,7 +171,7 @@ class AccountService:
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 AccountPasswordError("Invalid email or password.") raise AccountNotFoundError()
if account.status == AccountStatus.BANNED.value: if account.status == AccountStatus.BANNED.value:
raise AccountLoginError("Account is banned.") raise AccountLoginError("Account is banned.")
@ -438,7 +433,6 @@ class AccountService:
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:
@ -451,54 +445,14 @@ class AccountService:
code, token = cls.generate_reset_password_token(account_email, account) code, token = cls.generate_reset_password_token(account_email, account)
if account:
send_reset_password_mail_task.delay( send_reset_password_mail_task.delay(
language=language, language=language,
to=account_email, to=account_email,
code=code, 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()
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,
)
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,
@ -631,19 +585,6 @@ class AccountService:
) )
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,
@ -682,10 +623,6 @@ class AccountService:
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")
@ -698,10 +635,6 @@ class AccountService:
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")
@ -809,16 +742,6 @@ class AccountService:
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:
@ -838,24 +761,6 @@ 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=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):

View file

@ -1,86 +0,0 @@
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) -> 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,
},
)
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)

View file

@ -4,7 +4,6 @@ import time
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
@ -45,47 +44,3 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
) )
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)

View file

@ -1,87 +0,0 @@
<!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>

View file

@ -1,87 +0,0 @@
<!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>

View file

@ -1,94 +0,0 @@
<!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">It looks like you’re signing up with an existing account</p>
<p class="description">Hi,
We noticed you tried to sign up, but this email is already registered with an existing account.
Please log in here: </p>
<p class="description">
<a href="{{ login_url }}">Log In</a>
</p>
<p class="description">
If you forgot your password, you can reset it here: </p>
<p class="description">
<a href="{{ reset_password_url }}">Reset Password</a>
</p>
<p class="tips">If you didn’t request this action, you can safely ignore this email.
Need help? Feel free to contact us at support@dify.ai.</p>
</div>
</body>
</html>

View file

@ -1,95 +0,0 @@
<!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">您似乎正在使用现有账户注册</p>
<p class="description">Hi,
我们注意到您尝试注册,但此电子邮件已与现有账户注册。
请在此登录: </p>
<p class="description">
<a href="{{ login_url }}">登录</a>
</p>
<p class="description">
如果您忘记了密码,可以在此重置: </p>
<p class="description">
<a href="{{ reset_password_url }}">重置密码</a>
</p>
<p class="tips">如果您没有请求此操作,您可以安全地忽略此电子邮件。
需要帮助?随时联系我们 at support@dify.ai。</p>
</div>
</body>
</html>

View file

@ -1,85 +0,0 @@
<!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">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi,
We noticed you tried to reset your password, but this email is not associated with any account.
</p>
<p class="tips">If you didn’t request this action, you can safely ignore this email.
Need help? Feel free to contact us at support@dify.ai.</p>
</div>
</body>
</html>

View file

@ -1,84 +0,0 @@
<!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">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">Hi,
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。</p>
<p class="tips">如果您没有请求此操作,您可以安全地忽略此电子邮件。
需要帮助?随时联系我们 at support@dify.ai。</p>
</div>
</body>
</html>

View file

@ -1,89 +0,0 @@
<!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">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi,
We noticed you tried to reset your password, but this email is not associated with any account.
Please sign up here: </p>
<p class="description">
<a href="{{ sign_up_url }}">[Sign Up]</a>
</p>
<p class="tips">If you didn’t request this action, you can safely ignore this email.
Need help? Feel free to contact us at support@dify.ai.</p>
</div>
</body>
</html>

View file

@ -1,89 +0,0 @@
<!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">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">Hi,
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
请在此注册: </p>
<p class="description">
<a href="{{ sign_up_url }}">[注册]</a>
</p>
<p class="tips">如果您没有请求此操作,您可以安全地忽略此电子邮件。
需要帮助?随时联系我们 at support@dify.ai。</p>
</div>
</body>
</html>

View file

@ -1,83 +0,0 @@
<!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 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>

View file

@ -1,83 +0,0 @@
<!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>

View file

@ -1,90 +0,0 @@
<!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">It looks like you’re signing up with an existing account</p>
<p class="description">Hi,
We noticed you tried to sign up, but this email is already registered with an existing account.
Please log in here: </p>
<p class="description">
<a href="{{ login_url }}">Log In</a>
</p>
<p class="description">
If you forgot your password, you can reset it here: </p>
<p class="description">
<a href="{{ reset_password_url }}">Reset Password</a>
</p>
<p class="tips">If you didn’t request this action, you can safely ignore this email.
Need help? Feel free to contact us at support@dify.ai.</p>
</div>
</body>
</html>

View file

@ -1,91 +0,0 @@
<!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">您似乎正在使用现有账户注册</p>
<p class="description">Hi,
我们注意到您尝试注册,但此电子邮件已与现有账户注册。
请在此登录: </p>
<p class="description">
<a href="{{ login_url }}">登录</a>
</p>
<p class="description">
如果您忘记了密码,可以在此重置: </p>
<p class="description">
<a href="{{ reset_password_url }}">重置密码</a>
</p>
<p class="tips">如果您没有请求此操作,您可以安全地忽略此电子邮件。
需要帮助?随时联系我们 at support@dify.ai。</p>
</div>
</body>
</html>

View file

@ -1,81 +0,0 @@
<!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">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi,
We noticed you tried to reset your password, but this email is not associated with any account.
</p>
<p class="tips">If you didn’t request this action, you can safely ignore this email.
Need help? Feel free to contact us at support@dify.ai.</p>
</div>
</body>
</html>

View file

@ -1,81 +0,0 @@
<!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">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">Hi,
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
</p>
<p class="tips">如果您没有请求此操作,您可以安全地忽略此电子邮件。
需要帮助?随时联系我们 at support@dify.ai。</p>
</div>
</body>
</html>

View file

@ -1,85 +0,0 @@
<!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">It looks like you’re resetting a password with an unregistered email</p>
<p class="description">Hi,
We noticed you tried to reset your password, but this email is not associated with any account.
Please sign up here: </p>
<p class="description">
<a href="{{ sign_up_url }}">[Sign Up]</a>
</p>
<p class="tips">If you didn’t request this action, you can safely ignore this email.
Need help? Feel free to contact us at support@dify.ai.</p>
</div>
</body>
</html>

View file

@ -1,85 +0,0 @@
<!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">看起来您正在使用未注册的电子邮件重置密码</p>
<p class="description">Hi,
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
请在此注册: </p>
<p class="description">
<a href="{{ sign_up_url }}">[注册]</a>
</p>
<p class="tips">如果您没有请求此操作,您可以安全地忽略此电子邮件。
需要帮助?随时联系我们 at support@dify.ai。</p>
</div>
</body>
</html>

View file

@ -203,7 +203,6 @@ 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
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

View file

@ -13,6 +13,7 @@ from services.account_service import AccountService, RegisterService, TenantServ
from services.errors.account import ( from services.errors.account import (
AccountAlreadyInTenantError, AccountAlreadyInTenantError,
AccountLoginError, AccountLoginError,
AccountNotFoundError,
AccountPasswordError, AccountPasswordError,
AccountRegisterError, AccountRegisterError,
CurrentPasswordIncorrectError, CurrentPasswordIncorrectError,
@ -138,7 +139,7 @@ class TestAccountService:
fake = Faker() fake = Faker()
email = fake.email() email = fake.email()
password = fake.password(length=12) password = fake.password(length=12)
with pytest.raises(AccountPasswordError): with pytest.raises(AccountNotFoundError):
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):

View file

@ -9,6 +9,7 @@ from flask_restx import Api
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:
@ -26,33 +27,31 @@ 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.add_login_error_rate_limit") @patch("controllers.console.auth.login.AccountService.send_reset_password_email")
@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_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db self, mock_get_invitation, mock_send_email, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
): ):
"""Test that invalid email raises AuthenticationFailedError when account not found.""" """Test that invalid email sends reset password email when registration is allowed."""
# 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.AccountPasswordError("Invalid email or password.") mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
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
with pytest.raises(AuthenticationFailedError) as exc_info: assert result == {"result": "fail", "data": "token123", "code": "account_not_found"}
login_api.post() mock_send_email.assert_called_once_with(email="nonexistent@example.com", language="en-US")
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")
@ -88,17 +87,16 @@ 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.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_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db self, mock_get_invitation, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
): ):
"""Test that invalid email raises AuthenticationFailedError when account not found.""" """Test that invalid email raises AccountNotFound when registration is disabled."""
# 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.AccountPasswordError("Invalid email or password.") mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
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
@ -109,12 +107,10 @@ class TestAuthenticationSecurity:
login_api = LoginApi() login_api = LoginApi()
# Assert # Assert
with pytest.raises(AuthenticationFailedError) as exc_info: with pytest.raises(AccountNotFound) as exc_info:
login_api.post() login_api.post()
assert exc_info.value.error_code == "authentication_failed" assert exc_info.value.error_code == "account_not_found"
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")

View file

@ -10,6 +10,7 @@ from services.account_service import AccountService, RegisterService, TenantServ
from services.errors.account import ( from services.errors.account import (
AccountAlreadyInTenantError, AccountAlreadyInTenantError,
AccountLoginError, AccountLoginError,
AccountNotFoundError,
AccountPasswordError, AccountPasswordError,
AccountRegisterError, AccountRegisterError,
CurrentPasswordIncorrectError, CurrentPasswordIncorrectError,
@ -194,7 +195,7 @@ class TestAccountService:
# Execute test and verify exception # Execute test and verify exception
self._assert_exception_raised( self._assert_exception_raised(
AccountPasswordError, AccountService.authenticate, "notfound@example.com", "password" AccountNotFoundError, AccountService.authenticate, "notfound@example.com", "password"
) )
def test_authenticate_account_banned(self, mock_db_dependencies): def test_authenticate_account_banned(self, mock_db_dependencies):

View file

@ -843,7 +843,6 @@ 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
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

View file

@ -372,7 +372,6 @@ 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}
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}