Co-authored-by: NFish <douxc512@gmail.com>tags/0.15.0
| ) | ) | ||||
| class AccountConfig(BaseSettings): | |||||
| ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field( | |||||
| description="Duration in minutes for which a account deletion token remains valid", | |||||
| default=5, | |||||
| ) | |||||
| class FeatureConfig( | class FeatureConfig( | ||||
| # place the configs in alphabet order | # place the configs in alphabet order | ||||
| AppExecutionConfig, | AppExecutionConfig, | ||||
| WorkflowNodeExecutionConfig, | WorkflowNodeExecutionConfig, | ||||
| WorkspaceConfig, | WorkspaceConfig, | ||||
| LoginConfig, | LoginConfig, | ||||
| AccountConfig, | |||||
| # hosted services config | # hosted services config | ||||
| HostedServiceConfig, | HostedServiceConfig, | ||||
| CeleryBeatConfig, | CeleryBeatConfig, |
| error_code = "email_code_login_rate_limit_exceeded" | error_code = "email_code_login_rate_limit_exceeded" | ||||
| description = "Too many login emails have been sent. Please try again in 5 minutes." | description = "Too many login emails have been sent. Please try again in 5 minutes." | ||||
| code = 429 | code = 429 | ||||
| class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException): | |||||
| error_code = "email_code_account_deletion_rate_limit_exceeded" | |||||
| description = "Too many account deletion emails have been sent. Please try again in 5 minutes." | |||||
| code = 429 |
| from constants.languages import languages | from constants.languages import languages | ||||
| from controllers.console import api | from controllers.console import api | ||||
| from controllers.console.auth.error import ( | |||||
| EmailCodeError, | |||||
| InvalidEmailError, | |||||
| InvalidTokenError, | |||||
| PasswordMismatchError, | |||||
| ) | |||||
| from controllers.console.error import AccountNotFound, EmailSendIpLimitError | |||||
| from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError | |||||
| from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError | |||||
| from controllers.console.wraps import setup_required | from controllers.console.wraps import setup_required | ||||
| from events.tenant_event import tenant_was_created | from events.tenant_event import tenant_was_created | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from libs.password import hash_password, valid_password | from libs.password import hash_password, valid_password | ||||
| from models.account import Account | from models.account import Account | ||||
| from services.account_service import AccountService, TenantService | from services.account_service import AccountService, TenantService | ||||
| from services.errors.account import AccountRegisterError | |||||
| from services.errors.workspace import WorkSpaceNotAllowedCreateError | from services.errors.workspace import WorkSpaceNotAllowedCreateError | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| ) | ) | ||||
| except WorkSpaceNotAllowedCreateError: | except WorkSpaceNotAllowedCreateError: | ||||
| pass | pass | ||||
| except AccountRegisterError as are: | |||||
| raise AccountInFreezeError() | |||||
| return {"result": "success"} | return {"result": "success"} | ||||
| from flask_restful import Resource, reqparse # type: ignore | from flask_restful import Resource, reqparse # type: ignore | ||||
| import services | import services | ||||
| from configs import dify_config | |||||
| from constants.languages import languages | 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 ( | ||||
| ) | ) | ||||
| from controllers.console.error import ( | from controllers.console.error import ( | ||||
| AccountBannedError, | AccountBannedError, | ||||
| AccountInFreezeError, | |||||
| AccountNotFound, | AccountNotFound, | ||||
| EmailSendIpLimitError, | EmailSendIpLimitError, | ||||
| NotAllowedCreateWorkspace, | NotAllowedCreateWorkspace, | ||||
| from libs.password import valid_password | 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.errors.account import AccountRegisterError | |||||
| from services.errors.workspace import WorkSpaceNotAllowedCreateError | from services.errors.workspace import WorkSpaceNotAllowedCreateError | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| parser.add_argument("language", type=str, required=False, default="en-US", 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"]): | |||||
| raise AccountInFreezeError() | |||||
| is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) | is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) | ||||
| if is_login_error_rate_limit: | if is_login_error_rate_limit: | ||||
| raise EmailPasswordLoginLimitError() | raise EmailPasswordLoginLimitError() | ||||
| language = "zh-Hans" | language = "zh-Hans" | ||||
| else: | else: | ||||
| language = "en-US" | language = "en-US" | ||||
| account = AccountService.get_user_through_email(args["email"]) | |||||
| try: | |||||
| account = AccountService.get_user_through_email(args["email"]) | |||||
| except AccountRegisterError as are: | |||||
| raise AccountInFreezeError() | |||||
| if account is None: | if account is None: | ||||
| if FeatureService.get_system_features().is_allow_register: | if FeatureService.get_system_features().is_allow_register: | ||||
| token = AccountService.send_reset_password_email(email=args["email"], language=language) | token = AccountService.send_reset_password_email(email=args["email"], language=language) | ||||
| language = "zh-Hans" | language = "zh-Hans" | ||||
| else: | else: | ||||
| language = "en-US" | language = "en-US" | ||||
| try: | |||||
| account = AccountService.get_user_through_email(args["email"]) | |||||
| except AccountRegisterError as are: | |||||
| raise AccountInFreezeError() | |||||
| account = AccountService.get_user_through_email(args["email"]) | |||||
| if account is None: | if account is None: | ||||
| if FeatureService.get_system_features().is_allow_register: | if FeatureService.get_system_features().is_allow_register: | ||||
| token = AccountService.send_email_code_login_email(email=args["email"], language=language) | token = AccountService.send_email_code_login_email(email=args["email"], language=language) | ||||
| raise EmailCodeError() | raise EmailCodeError() | ||||
| AccountService.revoke_email_code_login_token(args["token"]) | AccountService.revoke_email_code_login_token(args["token"]) | ||||
| account = AccountService.get_user_through_email(user_email) | |||||
| try: | |||||
| account = AccountService.get_user_through_email(user_email) | |||||
| except AccountRegisterError as are: | |||||
| raise AccountInFreezeError() | |||||
| if account: | if account: | ||||
| tenant = TenantService.get_join_tenants(account) | tenant = TenantService.get_join_tenants(account) | ||||
| if not tenant: | if not tenant: | ||||
| ) | ) | ||||
| except WorkSpaceNotAllowedCreateError: | except WorkSpaceNotAllowedCreateError: | ||||
| return NotAllowedCreateWorkspace() | return NotAllowedCreateWorkspace() | ||||
| except AccountRegisterError as are: | |||||
| raise AccountInFreezeError() | |||||
| token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) | token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) | ||||
| AccountService.reset_login_error_rate_limit(args["email"]) | AccountService.reset_login_error_rate_limit(args["email"]) | ||||
| return {"result": "success", "data": token_pair.model_dump()} | return {"result": "success", "data": token_pair.model_dump()} |
| from models import Account | from models import Account | ||||
| from models.account import AccountStatus | from models.account import AccountStatus | ||||
| from services.account_service import AccountService, RegisterService, TenantService | from services.account_service import AccountService, RegisterService, TenantService | ||||
| from services.errors.account import AccountNotFoundError | |||||
| from services.errors.account import AccountNotFoundError, AccountRegisterError | |||||
| from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError | from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| f"{dify_config.CONSOLE_WEB_URL}/signin" | f"{dify_config.CONSOLE_WEB_URL}/signin" | ||||
| "?message=Workspace not found, please contact system admin to invite you to join in a workspace." | "?message=Workspace not found, please contact system admin to invite you to join in a workspace." | ||||
| ) | ) | ||||
| except AccountRegisterError as e: | |||||
| return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}") | |||||
| # Check account status | # Check account status | ||||
| if account.status == AccountStatus.BANNED.value: | if account.status == AccountStatus.BANNED.value: |
| error_code = "unauthorized_and_force_logout" | error_code = "unauthorized_and_force_logout" | ||||
| description = "Unauthorized and force logout." | description = "Unauthorized and force logout." | ||||
| code = 401 | code = 401 | ||||
| class AccountInFreezeError(BaseHTTPException): | |||||
| error_code = "account_in_freeze" | |||||
| code = 400 | |||||
| description = ( | |||||
| "This email account has been deleted within the past 30 days" | |||||
| "and is temporarily unavailable for new account registration." | |||||
| ) |
| from controllers.console.workspace.error import ( | from controllers.console.workspace.error import ( | ||||
| AccountAlreadyInitedError, | AccountAlreadyInitedError, | ||||
| CurrentPasswordIncorrectError, | CurrentPasswordIncorrectError, | ||||
| InvalidAccountDeletionCodeError, | |||||
| InvalidInvitationCodeError, | InvalidInvitationCodeError, | ||||
| RepeatPasswordNotMatchError, | RepeatPasswordNotMatchError, | ||||
| ) | ) | ||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from models import AccountIntegrate, InvitationCode | from models import AccountIntegrate, InvitationCode | ||||
| from services.account_service import AccountService | from services.account_service import AccountService | ||||
| from services.billing_service import BillingService | |||||
| from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError | from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError | ||||
| return {"data": integrate_data} | return {"data": integrate_data} | ||||
| class AccountDeleteVerifyApi(Resource): | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| def get(self): | |||||
| account = current_user | |||||
| token, code = AccountService.generate_account_deletion_verification_code(account) | |||||
| AccountService.send_account_deletion_verification_email(account, code) | |||||
| return {"result": "success", "data": token} | |||||
| class AccountDeleteApi(Resource): | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| def post(self): | |||||
| account = current_user | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("token", type=str, required=True, location="json") | |||||
| parser.add_argument("code", type=str, required=True, location="json") | |||||
| args = parser.parse_args() | |||||
| if not AccountService.verify_account_deletion_code(args["token"], args["code"]): | |||||
| raise InvalidAccountDeletionCodeError() | |||||
| AccountService.delete_account(account) | |||||
| return {"result": "success"} | |||||
| class AccountDeleteUpdateFeedbackApi(Resource): | |||||
| @setup_required | |||||
| def post(self): | |||||
| account = current_user | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("email", type=str, required=True, location="json") | |||||
| parser.add_argument("feedback", type=str, required=True, location="json") | |||||
| args = parser.parse_args() | |||||
| BillingService.update_account_deletion_feedback(args["email"], args["feedback"]) | |||||
| return {"result": "success"} | |||||
| # Register API resources | # Register API resources | ||||
| api.add_resource(AccountInitApi, "/account/init") | api.add_resource(AccountInitApi, "/account/init") | ||||
| api.add_resource(AccountProfileApi, "/account/profile") | api.add_resource(AccountProfileApi, "/account/profile") | ||||
| api.add_resource(AccountTimezoneApi, "/account/timezone") | api.add_resource(AccountTimezoneApi, "/account/timezone") | ||||
| api.add_resource(AccountPasswordApi, "/account/password") | api.add_resource(AccountPasswordApi, "/account/password") | ||||
| api.add_resource(AccountIntegrateApi, "/account/integrates") | api.add_resource(AccountIntegrateApi, "/account/integrates") | ||||
| api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") | |||||
| api.add_resource(AccountDeleteApi, "/account/delete") | |||||
| api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback") | |||||
| # api.add_resource(AccountEmailApi, '/account/email') | # api.add_resource(AccountEmailApi, '/account/email') | ||||
| # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') | # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') |
| error_code = "account_not_initialized" | error_code = "account_not_initialized" | ||||
| description = "The account has not been initialized yet. Please proceed with the initialization process first." | description = "The account has not been initialized yet. Please proceed with the initialization process first." | ||||
| code = 400 | code = 400 | ||||
| class InvalidAccountDeletionCodeError(BaseHTTPException): | |||||
| error_code = "invalid_account_deletion_code" | |||||
| description = "Invalid account deletion code." | |||||
| code = 400 |
| TenantStatus, | TenantStatus, | ||||
| ) | ) | ||||
| from models.model import DifySetup | from models.model import DifySetup | ||||
| from services.billing_service import BillingService | |||||
| from services.errors.account import ( | from services.errors.account import ( | ||||
| AccountAlreadyInTenantError, | AccountAlreadyInTenantError, | ||||
| AccountLoginError, | AccountLoginError, | ||||
| ) | ) | ||||
| from services.errors.workspace import WorkSpaceNotAllowedCreateError | from services.errors.workspace import WorkSpaceNotAllowedCreateError | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| from tasks.delete_account_task import delete_account_task | |||||
| from tasks.mail_account_deletion_task import send_account_deletion_verification_code | |||||
| 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 | ||||
| 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 | ||||
| ) | ) | ||||
| email_code_account_deletion_rate_limiter = RateLimiter( | |||||
| prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1 | |||||
| ) | |||||
| LOGIN_MAX_ERROR_LIMITS = 5 | LOGIN_MAX_ERROR_LIMITS = 5 | ||||
| @staticmethod | @staticmethod | ||||
| from controllers.console.error import AccountNotFound | from controllers.console.error import AccountNotFound | ||||
| raise AccountNotFound() | raise AccountNotFound() | ||||
| if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): | |||||
| raise AccountRegisterError( | |||||
| description=( | |||||
| "This email account has been deleted within the past " | |||||
| "30 days and is temporarily unavailable for new account registration" | |||||
| ) | |||||
| ) | |||||
| account = Account() | account = Account() | ||||
| account.email = email | account.email = email | ||||
| account.name = name | account.name = name | ||||
| return account | return account | ||||
| @staticmethod | |||||
| def generate_account_deletion_verification_code(account: Account) -> tuple[str, str]: | |||||
| code = "".join([str(random.randint(0, 9)) for _ in range(6)]) | |||||
| token = TokenManager.generate_token( | |||||
| account=account, token_type="account_deletion", additional_data={"code": code} | |||||
| ) | |||||
| return token, code | |||||
| @classmethod | |||||
| def send_account_deletion_verification_email(cls, account: Account, code: str): | |||||
| email = account.email | |||||
| if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): | |||||
| from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError | |||||
| raise EmailCodeAccountDeletionRateLimitExceededError() | |||||
| send_account_deletion_verification_code.delay(to=email, code=code) | |||||
| cls.email_code_account_deletion_rate_limiter.increment_rate_limit(email) | |||||
| @staticmethod | |||||
| def verify_account_deletion_code(token: str, code: str) -> bool: | |||||
| token_data = TokenManager.get_token_data(token, "account_deletion") | |||||
| if token_data is None: | |||||
| return False | |||||
| if token_data["code"] != code: | |||||
| return False | |||||
| return True | |||||
| @staticmethod | |||||
| def delete_account(account: Account) -> None: | |||||
| """Delete account. This method only adds a task to the queue for deletion.""" | |||||
| delete_account_task.delay(account.id) | |||||
| @staticmethod | @staticmethod | ||||
| def link_account_integrate(provider: str, open_id: str, account: Account) -> None: | def link_account_integrate(provider: str, open_id: str, account: Account) -> None: | ||||
| """Link account integrate""" | """Link account integrate""" | ||||
| def send_email_code_login_email( | def send_email_code_login_email( | ||||
| cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" | cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" | ||||
| ): | ): | ||||
| email = account.email if account else email | |||||
| if email is None: | if email is None: | ||||
| raise ValueError("Email must be provided.") | raise ValueError("Email must be provided.") | ||||
| if cls.email_code_login_rate_limiter.is_rate_limited(email): | if cls.email_code_login_rate_limiter.is_rate_limited(email): | ||||
| @classmethod | @classmethod | ||||
| def get_user_through_email(cls, email: str): | def get_user_through_email(cls, email: str): | ||||
| if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): | |||||
| raise AccountRegisterError( | |||||
| description=( | |||||
| "This email account has been deleted within the past " | |||||
| "30 days and is temporarily unavailable for new account registration" | |||||
| ) | |||||
| ) | |||||
| account = db.session.query(Account).filter(Account.email == email).first() | account = db.session.query(Account).filter(Account.email == email).first() | ||||
| if not account: | if not account: | ||||
| return None | return None | ||||
| db.session.commit() | db.session.commit() | ||||
| except WorkSpaceNotAllowedCreateError: | except WorkSpaceNotAllowedCreateError: | ||||
| db.session.rollback() | db.session.rollback() | ||||
| except AccountRegisterError as are: | |||||
| db.session.rollback() | |||||
| logging.exception("Register failed") | |||||
| raise are | |||||
| except Exception as e: | except Exception as e: | ||||
| db.session.rollback() | db.session.rollback() | ||||
| logging.exception("Register failed") | logging.exception("Register failed") |
| if not TenantAccountRole.is_privileged_role(join.role): | if not TenantAccountRole.is_privileged_role(join.role): | ||||
| raise ValueError("Only team owner or team admin can perform this action") | raise ValueError("Only team owner or team admin can perform this action") | ||||
| @classmethod | |||||
| def delete_account(cls, account_id: str): | |||||
| """Delete account.""" | |||||
| params = {"account_id": account_id} | |||||
| return cls._send_request("DELETE", "/account/", params=params) | |||||
| @classmethod | |||||
| def is_email_in_freeze(cls, email: str) -> bool: | |||||
| params = {"email": email} | |||||
| try: | |||||
| response = cls._send_request("GET", "/account/in-freeze", params=params) | |||||
| return bool(response.get("data", False)) | |||||
| except Exception: | |||||
| return False | |||||
| @classmethod | |||||
| def update_account_deletion_feedback(cls, email: str, feedback: str): | |||||
| """Update account deletion feedback.""" | |||||
| json = {"email": email, "feedback": feedback} | |||||
| return cls._send_request("POST", "/account/delete-feedback", json=json) |
| import logging | |||||
| from celery import shared_task # type: ignore | |||||
| from extensions.ext_database import db | |||||
| from models.account import Account | |||||
| from services.billing_service import BillingService | |||||
| from tasks.mail_account_deletion_task import send_deletion_success_task | |||||
| logger = logging.getLogger(__name__) | |||||
| @shared_task(queue="dataset") | |||||
| def delete_account_task(account_id): | |||||
| account = db.session.query(Account).filter(Account.id == account_id).first() | |||||
| try: | |||||
| BillingService.delete_account(account_id) | |||||
| except Exception as e: | |||||
| logger.exception(f"Failed to delete account {account_id} from billing service.") | |||||
| raise | |||||
| if not account: | |||||
| logger.error(f"Account {account_id} not found.") | |||||
| return | |||||
| # send success email | |||||
| send_deletion_success_task.delay(account.email) |
| import logging | |||||
| import time | |||||
| import click | |||||
| from celery import shared_task # type: ignore | |||||
| from flask import render_template | |||||
| from extensions.ext_mail import mail | |||||
| @shared_task(queue="mail") | |||||
| def send_deletion_success_task(to): | |||||
| """Send email to user regarding account deletion. | |||||
| Args: | |||||
| log (AccountDeletionLog): Account deletion log object | |||||
| """ | |||||
| if not mail.is_inited(): | |||||
| return | |||||
| logging.info(click.style(f"Start send account deletion success email to {to}", fg="green")) | |||||
| start_at = time.perf_counter() | |||||
| try: | |||||
| html_content = render_template( | |||||
| "delete_account_success_template_en-US.html", | |||||
| to=to, | |||||
| email=to, | |||||
| ) | |||||
| mail.send(to=to, subject="Your Dify.AI Account Has Been Successfully Deleted", html=html_content) | |||||
| end_at = time.perf_counter() | |||||
| logging.info( | |||||
| click.style( | |||||
| "Send account deletion success email to {}: latency: {}".format(to, end_at - start_at), fg="green" | |||||
| ) | |||||
| ) | |||||
| except Exception: | |||||
| logging.exception("Send account deletion success email to {} failed".format(to)) | |||||
| @shared_task(queue="mail") | |||||
| def send_account_deletion_verification_code(to, code): | |||||
| """Send email to user regarding account deletion verification code. | |||||
| Args: | |||||
| to (str): Recipient email address | |||||
| code (str): Verification code | |||||
| """ | |||||
| if not mail.is_inited(): | |||||
| return | |||||
| logging.info(click.style(f"Start send account deletion verification code email to {to}", fg="green")) | |||||
| start_at = time.perf_counter() | |||||
| try: | |||||
| html_content = render_template("delete_account_code_email_template_en-US.html", to=to, code=code) | |||||
| mail.send(to=to, subject="Dify.AI Account Deletion and Verification", html=html_content) | |||||
| end_at = time.perf_counter() | |||||
| logging.info( | |||||
| click.style( | |||||
| "Send account deletion verification code email to {} succeeded: latency: {}".format( | |||||
| to, end_at - start_at | |||||
| ), | |||||
| fg="green", | |||||
| ) | |||||
| ) | |||||
| except Exception: | |||||
| logging.exception("Send account deletion verification code email to {} failed".format(to)) |
| <!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; | |||||
| min-height: 605px; | |||||
| 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; | |||||
| } | |||||
| .typography { | |||||
| letter-spacing: -0.07px; | |||||
| font-weight: 400; | |||||
| font-style: normal; | |||||
| font-size: 14px; | |||||
| line-height: 20px; | |||||
| color: #354052; | |||||
| margin-top: 12px; | |||||
| margin-bottom: 12px; | |||||
| } | |||||
| .typography p{ | |||||
| margin: 0 auto; | |||||
| } | |||||
| .typography-title { | |||||
| color: #101828; | |||||
| font-size: 14px; | |||||
| font-style: normal; | |||||
| font-weight: 600; | |||||
| line-height: 20px; | |||||
| margin-top: 12px; | |||||
| margin-bottom: 4px; | |||||
| } | |||||
| .tip-list{ | |||||
| margin: 0; | |||||
| padding-left: 10px; | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div class="container"> | |||||
| <div class="header"> | |||||
| <!-- Optional: Add a logo or a header image here --> | |||||
| <img src="https://cloud.dify.ai/logo/logo-site.png" alt="Dify Logo" /> | |||||
| </div> | |||||
| <p class="title">Dify.AI Account Deletion and Verification</p> | |||||
| <p class="typography">We received a request to delete your Dify account. To ensure the security of your account and | |||||
| confirm this action, please use the verification code below:</p> | |||||
| <div class="code-content"> | |||||
| <span class="code">{{code}}</span> | |||||
| </div> | |||||
| <div class="typography"> | |||||
| <p style="margin-bottom:4px">To complete the account deletion process:</p> | |||||
| <p>1. Return to the account deletion page on our website</p> | |||||
| <p>2. Enter the verification code above</p> | |||||
| <p>3. Click "Confirm Deletion"</p> | |||||
| </div> | |||||
| <p class="typography-title">Please note:</p> | |||||
| <ul class="typography tip-list"> | |||||
| <li>This code is valid for 5 minutes</li> | |||||
| <li>As the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion.</li> | |||||
| <li>All your user data will be queued for permanent deletion.</li> | |||||
| </ul> | |||||
| </div> | |||||
| </body> | |||||
| </html> |
| <!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; | |||||
| min-height: 380px; | |||||
| 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; | |||||
| margin-bottom: 12px; | |||||
| } | |||||
| .description { | |||||
| color: #354052; | |||||
| font-weight: 400; | |||||
| line-height: 20px; | |||||
| font-size: 14px; | |||||
| } | |||||
| .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; | |||||
| } | |||||
| .email { | |||||
| color: #354052; | |||||
| font-weight: 600; | |||||
| line-height: 20px; | |||||
| font-size: 14px; | |||||
| } | |||||
| .typography{ | |||||
| font-weight: 400; | |||||
| font-style: normal; | |||||
| font-size: 14px; | |||||
| line-height: 20px; | |||||
| color: #354052; | |||||
| margin-top: 4px; | |||||
| margin-bottom: 0; | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div class="container"> | |||||
| <div class="header"> | |||||
| <!-- Optional: Add a logo or a header image here --> | |||||
| <img src="https://cloud.dify.ai/logo/logo-site.png" alt="Dify Logo" /> | |||||
| </div> | |||||
| <p class="title">Your Dify.AI Account Has Been Successfully Deleted</p> | |||||
| <p class="typography">We're writing to confirm that your Dify.AI account has been successfully deleted as per your request. Your | |||||
| account is no longer accessible, and you can't log in using your previous credentials. If you decide to use | |||||
| Dify.AI services in the future, you'll need to create a new account after 30 days. We appreciate the time you | |||||
| spent with Dify.AI and are sorry to see you go. If you have any questions or concerns about the deletion process, | |||||
| please don't hesitate to reach out to our support team.</p> | |||||
| <p class="typography">Thank you for being a part of the Dify.AI community.</p> | |||||
| <p class="typography">Best regards,</p> | |||||
| <p class="typography">Dify.AI Team</p> | |||||
| </div> | |||||
| </body> | |||||
| </html> |