| @@ -530,7 +530,6 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id} | |||
| # Reset password token expiry minutes | |||
| RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 | |||
| EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5 | |||
| CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 | |||
| OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 | |||
| @@ -31,12 +31,6 @@ class SecurityConfig(BaseSettings): | |||
| description="Duration in minutes for which a password reset token remains valid", | |||
| 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( | |||
| description="Duration in minutes for which a change email token remains valid", | |||
| default=5, | |||
| @@ -645,11 +639,6 @@ class AuthConfig(BaseSettings): | |||
| 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): | |||
| """ | |||
| @@ -70,16 +70,7 @@ from .app import ( | |||
| ) | |||
| # Import auth controllers | |||
| from .auth import ( | |||
| activate, | |||
| data_source_bearer_auth, | |||
| data_source_oauth, | |||
| email_register, | |||
| forgot_password, | |||
| login, | |||
| oauth, | |||
| oauth_server, | |||
| ) | |||
| from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth, oauth_server | |||
| # Import billing controllers | |||
| from .billing import billing, compliance | |||
| @@ -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") | |||
| @@ -31,12 +31,6 @@ class PasswordResetRateLimitExceededError(BaseHTTPException): | |||
| 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): | |||
| error_code = "email_change_rate_limit_exceeded" | |||
| description = "Too many email change emails have been sent. Please try again in 1 minute." | |||
| @@ -91,12 +85,6 @@ class EmailPasswordResetLimitError(BaseHTTPException): | |||
| 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): | |||
| error_code = "email_change_limit" | |||
| description = "Too many failed email change attempts. Please try again in 24 hours." | |||
| @@ -6,6 +6,7 @@ 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 ( | |||
| EmailCodeError, | |||
| @@ -14,7 +15,7 @@ from controllers.console.auth.error import ( | |||
| InvalidTokenError, | |||
| 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 events.tenant_event import tenant_was_created | |||
| 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 models.account import Account | |||
| 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 | |||
| @@ -45,13 +48,15 @@ class ForgotPasswordSendEmailApi(Resource): | |||
| with Session(db.engine) as session: | |||
| account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none() | |||
| token = AccountService.send_reset_password_email( | |||
| account=account, | |||
| email=args["email"], | |||
| language=language, | |||
| is_allow_register=FeatureService.get_system_features().is_allow_register, | |||
| ) | |||
| 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) | |||
| return {"result": "success", "data": token} | |||
| @@ -132,7 +137,7 @@ class ForgotPasswordResetApi(Resource): | |||
| if account: | |||
| self._update_existing_account(account, password_hashed, salt, session) | |||
| else: | |||
| raise AccountNotFound() | |||
| self._create_new_account(email, args["password_confirm"]) | |||
| return {"result": "success"} | |||
| @@ -152,6 +157,22 @@ class ForgotPasswordResetApi(Resource): | |||
| account.current_tenant = 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") | |||
| @@ -26,6 +26,7 @@ from controllers.console.error import ( | |||
| from controllers.console.wraps import email_password_login_enabled, setup_required | |||
| from events.tenant_event import tenant_was_created | |||
| 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, RegisterService, TenantService | |||
| from services.billing_service import BillingService | |||
| @@ -43,9 +44,10 @@ class LoginApi(Resource): | |||
| """Authenticate user and login.""" | |||
| parser = reqparse.RequestParser() | |||
| 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("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() | |||
| if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]): | |||
| @@ -59,6 +61,11 @@ class LoginApi(Resource): | |||
| if 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: | |||
| if invitation: | |||
| data = invitation.get("data", {}) | |||
| @@ -73,6 +80,12 @@ class LoginApi(Resource): | |||
| except services.errors.account.AccountPasswordError: | |||
| AccountService.add_login_error_rate_limit(args["email"]) | |||
| 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 | |||
| tenants = TenantService.get_join_tenants(account) | |||
| if len(tenants) == 0: | |||
| @@ -120,12 +133,13 @@ class ResetPasswordSendEmailApi(Resource): | |||
| except AccountRegisterError: | |||
| raise AccountInFreezeError() | |||
| token = AccountService.send_reset_password_email( | |||
| email=args["email"], | |||
| account=account, | |||
| language=language, | |||
| is_allow_register=FeatureService.get_system_features().is_allow_register, | |||
| ) | |||
| 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) | |||
| return {"result": "success", "data": token} | |||
| @@ -242,19 +242,6 @@ def email_password_login_enabled(view: Callable[P, R]): | |||
| 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]): | |||
| @wraps(view) | |||
| def decorated(*args: P.args, **kwargs: P.kwargs): | |||
| @@ -21,7 +21,6 @@ class EmailType(Enum): | |||
| """Enumeration of supported email types.""" | |||
| RESET_PASSWORD = "reset_password" | |||
| RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST = "reset_password_when_account_not_exist" | |||
| INVITE_MEMBER = "invite_member" | |||
| EMAIL_CODE_LOGIN = "email_code_login" | |||
| CHANGE_EMAIL_OLD = "change_email_old" | |||
| @@ -35,9 +34,6 @@ class EmailType(Enum): | |||
| ENTERPRISE_CUSTOM = "enterprise_custom" | |||
| QUEUE_MONITOR_ALERT = "queue_monitor_alert" | |||
| 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): | |||
| @@ -445,54 +441,6 @@ def create_default_email_config() -> EmailI18nConfig: | |||
| 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) | |||
| @@ -37,6 +37,7 @@ from services.billing_service import BillingService | |||
| from services.errors.account import ( | |||
| AccountAlreadyInTenantError, | |||
| AccountLoginError, | |||
| AccountNotFoundError, | |||
| AccountNotLinkTenantError, | |||
| AccountPasswordError, | |||
| AccountRegisterError, | |||
| @@ -64,11 +65,7 @@ from tasks.mail_owner_transfer_task import ( | |||
| send_old_owner_transfer_notify_email_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, | |||
| send_reset_password_mail_task_when_account_not_exist, | |||
| ) | |||
| from tasks.mail_reset_password_task import send_reset_password_mail_task | |||
| logger = logging.getLogger(__name__) | |||
| @@ -85,7 +82,6 @@ REFRESH_TOKEN_EXPIRY = timedelta(days=dify_config.REFRESH_TOKEN_EXPIRE_DAYS) | |||
| class AccountService: | |||
| 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( | |||
| 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 | |||
| CHANGE_EMAIL_MAX_ERROR_LIMITS = 5 | |||
| OWNER_TRANSFER_MAX_ERROR_LIMITS = 5 | |||
| EMAIL_REGISTER_MAX_ERROR_LIMITS = 5 | |||
| @staticmethod | |||
| 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() | |||
| if not account: | |||
| raise AccountPasswordError("Invalid email or password.") | |||
| raise AccountNotFoundError() | |||
| if account.status == AccountStatus.BANNED.value: | |||
| raise AccountLoginError("Account is banned.") | |||
| @@ -438,7 +433,6 @@ class AccountService: | |||
| account: Optional[Account] = None, | |||
| email: Optional[str] = None, | |||
| language: str = "en-US", | |||
| is_allow_register: bool = False, | |||
| ): | |||
| account_email = account.email if account else email | |||
| if account_email is None: | |||
| @@ -451,54 +445,14 @@ class AccountService: | |||
| code, token = cls.generate_reset_password_token(account_email, account) | |||
| 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, | |||
| ) | |||
| send_reset_password_mail_task.delay( | |||
| language=language, | |||
| to=account_email, | |||
| code=code, | |||
| ) | |||
| cls.reset_password_rate_limiter.increment_rate_limit(account_email) | |||
| 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 | |||
| def send_change_email_email( | |||
| cls, | |||
| @@ -631,19 +585,6 @@ class AccountService: | |||
| ) | |||
| 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 | |||
| def generate_change_email_token( | |||
| cls, | |||
| @@ -682,10 +623,6 @@ class AccountService: | |||
| def revoke_reset_password_token(cls, token: str): | |||
| TokenManager.revoke_token(token, "reset_password") | |||
| @classmethod | |||
| def revoke_email_register_token(cls, token: str): | |||
| TokenManager.revoke_token(token, "email_register") | |||
| @classmethod | |||
| def revoke_change_email_token(cls, token: str): | |||
| TokenManager.revoke_token(token, "change_email") | |||
| @@ -698,10 +635,6 @@ class AccountService: | |||
| def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]: | |||
| 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 | |||
| def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]: | |||
| return TokenManager.get_token_data(token, "change_email") | |||
| @@ -809,16 +742,6 @@ class AccountService: | |||
| count = int(count) + 1 | |||
| 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 | |||
| @redis_fallback(default_return=False) | |||
| def is_forgot_password_error_rate_limit(email: str) -> bool: | |||
| @@ -838,24 +761,6 @@ class AccountService: | |||
| key = f"forgot_password_error_rate_limit:{email}" | |||
| 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 | |||
| @redis_fallback(default_return=None) | |||
| def add_change_email_error_rate_limit(email: str): | |||
| @@ -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) | |||
| @@ -4,7 +4,6 @@ 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 | |||
| @@ -45,47 +44,3 @@ def send_reset_password_mail_task(language: str, to: str, code: str): | |||
| ) | |||
| except Exception: | |||
| 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) | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -203,7 +203,6 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id} | |||
| # Reset password token expiry minutes | |||
| RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 | |||
| EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5 | |||
| CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 | |||
| OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 | |||
| @@ -13,6 +13,7 @@ from services.account_service import AccountService, RegisterService, TenantServ | |||
| from services.errors.account import ( | |||
| AccountAlreadyInTenantError, | |||
| AccountLoginError, | |||
| AccountNotFoundError, | |||
| AccountPasswordError, | |||
| AccountRegisterError, | |||
| CurrentPasswordIncorrectError, | |||
| @@ -138,7 +139,7 @@ class TestAccountService: | |||
| fake = Faker() | |||
| email = fake.email() | |||
| password = fake.password(length=12) | |||
| with pytest.raises(AccountPasswordError): | |||
| with pytest.raises(AccountNotFoundError): | |||
| AccountService.authenticate(email, password) | |||
| def test_authenticate_banned_account(self, db_session_with_containers, mock_external_service_dependencies): | |||
| @@ -9,6 +9,7 @@ from flask_restx import Api | |||
| import services.errors.account | |||
| from controllers.console.auth.error import AuthenticationFailedError | |||
| from controllers.console.auth.login import LoginApi | |||
| from controllers.console.error import AccountNotFound | |||
| class TestAuthenticationSecurity: | |||
| @@ -26,33 +27,31 @@ class TestAuthenticationSecurity: | |||
| @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.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.RegisterService.get_invitation_if_token_valid") | |||
| 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 | |||
| mock_is_rate_limit.return_value = False | |||
| 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_features.return_value.is_allow_register = True | |||
| mock_send_email.return_value = "token123" | |||
| # Act | |||
| with self.app.test_request_context( | |||
| "/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"} | |||
| ): | |||
| login_api = LoginApi() | |||
| result = login_api.post() | |||
| # 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") | |||
| # 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") | |||
| @patch("controllers.console.wraps.db") | |||
| @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.AccountService.is_login_error_rate_limit") | |||
| @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.RegisterService.get_invitation_if_token_valid") | |||
| 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 | |||
| mock_is_rate_limit.return_value = False | |||
| 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_features.return_value.is_allow_register = False | |||
| @@ -109,12 +107,10 @@ class TestAuthenticationSecurity: | |||
| login_api = LoginApi() | |||
| # Assert | |||
| with pytest.raises(AuthenticationFailedError) as exc_info: | |||
| with pytest.raises(AccountNotFound) 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") | |||
| assert exc_info.value.error_code == "account_not_found" | |||
| @patch("controllers.console.wraps.db") | |||
| @patch("controllers.console.auth.login.FeatureService.get_system_features") | |||
| @@ -10,6 +10,7 @@ from services.account_service import AccountService, RegisterService, TenantServ | |||
| from services.errors.account import ( | |||
| AccountAlreadyInTenantError, | |||
| AccountLoginError, | |||
| AccountNotFoundError, | |||
| AccountPasswordError, | |||
| AccountRegisterError, | |||
| CurrentPasswordIncorrectError, | |||
| @@ -194,7 +195,7 @@ class TestAccountService: | |||
| # Execute test and verify exception | |||
| 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): | |||
| @@ -843,7 +843,6 @@ INVITE_EXPIRY_HOURS=72 | |||
| # Reset password token valid time (minutes), | |||
| RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 | |||
| EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5 | |||
| CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5 | |||
| OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5 | |||
| @@ -372,7 +372,6 @@ x-shared-env: &shared-api-worker-env | |||
| INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000} | |||
| INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} | |||
| 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} | |||
| OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: ${OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES:-5} | |||
| CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194} | |||