|
|
|
@ -52,6 +52,7 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError, Workspaces
|
|
|
|
from services.feature_service import FeatureService
|
|
|
|
from services.feature_service import FeatureService
|
|
|
|
from tasks.delete_account_task import delete_account_task
|
|
|
|
from tasks.delete_account_task import delete_account_task
|
|
|
|
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
|
|
|
|
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
|
|
|
|
|
|
|
|
from tasks.mail_change_mail_task import send_change_mail_task
|
|
|
|
from tasks.mail_email_code_login import send_email_code_login_mail_task
|
|
|
|
from tasks.mail_email_code_login import send_email_code_login_mail_task
|
|
|
|
from tasks.mail_invite_member_task import send_invite_member_mail_task
|
|
|
|
from tasks.mail_invite_member_task import send_invite_member_mail_task
|
|
|
|
from tasks.mail_reset_password_task import send_reset_password_mail_task
|
|
|
|
from tasks.mail_reset_password_task import send_reset_password_mail_task
|
|
|
|
@ -75,8 +76,12 @@ class AccountService:
|
|
|
|
email_code_account_deletion_rate_limiter = RateLimiter(
|
|
|
|
email_code_account_deletion_rate_limiter = RateLimiter(
|
|
|
|
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
|
|
|
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
change_email_rate_limiter = RateLimiter(
|
|
|
|
|
|
|
|
prefix="change_email_rate_limit", max_attempts=2, time_window=60 * 1
|
|
|
|
|
|
|
|
)
|
|
|
|
LOGIN_MAX_ERROR_LIMITS = 5
|
|
|
|
LOGIN_MAX_ERROR_LIMITS = 5
|
|
|
|
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
|
|
|
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
|
|
|
|
|
|
|
CHANGE_EMAIL_MAX_ERROR_LIMITS = 5
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
@staticmethod
|
|
|
|
def _get_refresh_token_key(refresh_token: str) -> str:
|
|
|
|
def _get_refresh_token_key(refresh_token: str) -> str:
|
|
|
|
@ -419,6 +424,35 @@ class AccountService:
|
|
|
|
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
|
|
|
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
|
|
|
return token
|
|
|
|
return token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def send_change_email_email(
|
|
|
|
|
|
|
|
cls,
|
|
|
|
|
|
|
|
account: Optional[Account] = None,
|
|
|
|
|
|
|
|
email: Optional[str] = None,
|
|
|
|
|
|
|
|
old_email: Optional[str] = None,
|
|
|
|
|
|
|
|
language: Optional[str] = "en-US",
|
|
|
|
|
|
|
|
phase: Optional[str] = None,
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
account_email = account.email if account else email
|
|
|
|
|
|
|
|
if account_email is None:
|
|
|
|
|
|
|
|
raise ValueError("Email must be provided.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if cls.change_email_rate_limiter.is_rate_limited(account_email):
|
|
|
|
|
|
|
|
from controllers.console.auth.error import EmailChangeRateLimitExceededError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
raise EmailChangeRateLimitExceededError()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
code, token = cls.generate_change_email_token(account_email, account, old_email=old_email)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
send_change_mail_task.delay(
|
|
|
|
|
|
|
|
language=language,
|
|
|
|
|
|
|
|
to=account_email,
|
|
|
|
|
|
|
|
code=code,
|
|
|
|
|
|
|
|
phase=phase,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
cls.change_email_rate_limiter.increment_rate_limit(account_email)
|
|
|
|
|
|
|
|
return token
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def generate_reset_password_token(
|
|
|
|
def generate_reset_password_token(
|
|
|
|
cls,
|
|
|
|
cls,
|
|
|
|
@ -435,13 +469,39 @@ class AccountService:
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return code, token
|
|
|
|
return code, token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def generate_change_email_token(
|
|
|
|
|
|
|
|
cls,
|
|
|
|
|
|
|
|
email: str,
|
|
|
|
|
|
|
|
account: Optional[Account] = None,
|
|
|
|
|
|
|
|
code: Optional[str] = None,
|
|
|
|
|
|
|
|
old_email: Optional[str] = None,
|
|
|
|
|
|
|
|
additional_data: dict[str, Any] = {},
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
if not code:
|
|
|
|
|
|
|
|
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
|
|
|
|
|
|
|
|
additional_data["code"] = code
|
|
|
|
|
|
|
|
additional_data["old_email"] = old_email
|
|
|
|
|
|
|
|
token = TokenManager.generate_token(
|
|
|
|
|
|
|
|
account=account, email=email, token_type="change_email", additional_data=additional_data
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return code, token
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def revoke_reset_password_token(cls, token: str):
|
|
|
|
def revoke_reset_password_token(cls, token: str):
|
|
|
|
TokenManager.revoke_token(token, "reset_password")
|
|
|
|
TokenManager.revoke_token(token, "reset_password")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def revoke_change_email_token(cls, token: str):
|
|
|
|
|
|
|
|
TokenManager.revoke_token(token, "change_email")
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
|
|
|
|
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
|
|
|
|
return TokenManager.get_token_data(token, "reset_password")
|
|
|
|
return TokenManager.get_token_data(token, "reset_password")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]:
|
|
|
|
|
|
|
|
return TokenManager.get_token_data(token, "change_email")
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def send_email_code_login_email(
|
|
|
|
def send_email_code_login_email(
|
|
|
|
@ -540,13 +600,42 @@ class AccountService:
|
|
|
|
if count > AccountService.FORGOT_PASSWORD_MAX_ERROR_LIMITS:
|
|
|
|
if count > AccountService.FORGOT_PASSWORD_MAX_ERROR_LIMITS:
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
@staticmethod
|
|
|
|
def reset_forgot_password_error_rate_limit(email: str):
|
|
|
|
def reset_forgot_password_error_rate_limit(email: str):
|
|
|
|
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
@redis_fallback(default_return=None)
|
|
|
|
|
|
|
|
def add_change_email_error_rate_limit(email: str) -> None:
|
|
|
|
|
|
|
|
key = f"change_email_error_rate_limit:{email}"
|
|
|
|
|
|
|
|
count = redis_client.get(key)
|
|
|
|
|
|
|
|
if count is None:
|
|
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
|
|
count = int(count) + 1
|
|
|
|
|
|
|
|
redis_client.setex(key, dify_config.CHANGE_EMAIL_LOCKOUT_DURATION, count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
@redis_fallback(default_return=False)
|
|
|
|
|
|
|
|
def is_change_email_error_rate_limit(email: str) -> bool:
|
|
|
|
|
|
|
|
key = f"change_email_error_rate_limit:{email}"
|
|
|
|
|
|
|
|
count = redis_client.get(key)
|
|
|
|
|
|
|
|
if count is None:
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
count = int(count)
|
|
|
|
|
|
|
|
if count > AccountService.CHANGE_EMAIL_MAX_ERROR_LIMITS:
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
@redis_fallback(default_return=None)
|
|
|
|
|
|
|
|
def reset_change_email_error_rate_limit(email: str):
|
|
|
|
|
|
|
|
key = f"change_email_error_rate_limit:{email}"
|
|
|
|
|
|
|
|
redis_client.delete(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
@redis_fallback(default_return=False)
|
|
|
|
def is_email_send_ip_limit(ip_address: str):
|
|
|
|
def is_email_send_ip_limit(ip_address: str):
|
|
|
|
minute_key = f"email_send_ip_limit_minute:{ip_address}"
|
|
|
|
minute_key = f"email_send_ip_limit_minute:{ip_address}"
|
|
|
|
freeze_key = f"email_send_ip_limit_freeze:{ip_address}"
|
|
|
|
freeze_key = f"email_send_ip_limit_freeze:{ip_address}"
|
|
|
|
|