From 11e7ecbc356e7a6788fbc616d2ef935fe2d97d69 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 18 Dec 2024 23:56:54 -0500 Subject: [PATCH 01/55] feat: account delete --- api/configs/feature/__init__.py | 24 ++-- api/controllers/console/workspace/account.py | 67 ++++++++-- api/libs/helper.py | 12 +- api/models/account.py | 14 ++ api/services/account_deletion_log_service.py | 34 +++++ api/services/account_service.py | 99 ++++++++++----- api/services/billing_service.py | 16 ++- api/tasks/delete_account_task.py | 67 ++++++++++ api/tasks/mail_account_deletion_task.py | 127 +++++++++++++++++++ 9 files changed, 398 insertions(+), 62 deletions(-) create mode 100644 api/services/account_deletion_log_service.py create mode 100644 api/tasks/delete_account_task.py create mode 100644 api/tasks/mail_account_deletion_task.py diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index f10252e45582c6..0f6a62ac73c262 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1,18 +1,10 @@ from typing import Annotated, Literal, Optional -from pydantic import ( - AliasChoices, - Field, - HttpUrl, - NegativeInt, - NonNegativeInt, - PositiveFloat, - PositiveInt, - computed_field, -) -from pydantic_settings import BaseSettings - from configs.feature.hosted_service import HostedServiceConfig +from pydantic import (AliasChoices, Field, HttpUrl, NegativeInt, + NonNegativeInt, PositiveFloat, PositiveInt, + computed_field) +from pydantic_settings import BaseSettings class SecurityConfig(BaseSettings): @@ -765,6 +757,13 @@ class LoginConfig(BaseSettings): ) +class RegisterConfig(BaseSettings): + EMAIL_FREEZE_PERIOD_IN_DAYS: PositiveInt = Field( + description="Freeze period in days for re-registering with the same email", + default=30, + ) + + class FeatureConfig( # place the configs in alphabet order AppExecutionConfig, @@ -792,6 +791,7 @@ class FeatureConfig( WorkflowNodeExecutionConfig, WorkspaceConfig, LoginConfig, + RegisterConfig, # hosted services config HostedServiceConfig, CeleryBeatConfig, diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 96ed4b7a570256..b256e7c8bb7292 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -1,27 +1,29 @@ import datetime import pytz -from flask import request -from flask_login import current_user # type: ignore -from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore - from configs import dify_config from constants.languages import supported_language from controllers.console import api -from controllers.console.workspace.error import ( - AccountAlreadyInitedError, - CurrentPasswordIncorrectError, - InvalidInvitationCodeError, - RepeatPasswordNotMatchError, -) -from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required +from controllers.console.workspace.error import (AccountAlreadyInitedError, + CurrentPasswordIncorrectError, + InvalidInvitationCodeError, + RepeatPasswordNotMatchError) +from controllers.console.wraps import (account_initialization_required, + enterprise_license_required, + setup_required) from extensions.ext_database import db from fields.member_fields import account_fields +from flask import request +from flask_login import current_user # type: ignore +from flask_restful import (Resource, fields, marshal_with, # type: ignore + reqparse) from libs.helper import TimestampField, timezone from libs.login import login_required from models import AccountIntegrate, InvitationCode from services.account_service import AccountService -from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError +from services.errors.account import \ + CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError +from services.errors.account import RateLimitExceededError class AccountInitApi(Resource): @@ -242,6 +244,45 @@ def get(self): return {"data": integrate_data} +class AccountDeleteVerifyApi(Resource): + + @setup_required + @login_required + @account_initialization_required + def post(self): + account = current_user + + try: + token, code = AccountService.generate_account_deletion_verification_code(account) + AccountService.send_account_delete_verification_email(account, code) + except RateLimitExceededError: + return {"result": "fail", "error": "Rate limit exceeded."}, 429 + + 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") + parser.add_argument("reason", type=str, required=True, location="json") + args = parser.parse_args() + + if not AccountService.verify_account_deletion_code(args["token"], args["code"]): + return {"result": "fail", "error": "Verification code is invalid."}, 400 + + AccountService.delete_account(account, args["reason"]) + + return {"result": "success"} + + # Register API resources api.add_resource(AccountInitApi, "/account/init") api.add_resource(AccountProfileApi, "/account/profile") @@ -252,5 +293,7 @@ def get(self): api.add_resource(AccountTimezoneApi, "/account/timezone") api.add_resource(AccountPasswordApi, "/account/password") api.add_resource(AccountIntegrateApi, "/account/integrates") +api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") +api.add_resource(AccountDeleteApi, "/account/delete") # api.add_resource(AccountEmailApi, '/account/email') # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') diff --git a/api/libs/helper.py b/api/libs/helper.py index eaa4efdb714355..8f315078e6a911 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -8,19 +8,21 @@ import uuid from collections.abc import Generator, Mapping from datetime import datetime +from datetime import timezone as tz from hashlib import sha256 from typing import Any, Optional, Union, cast from zoneinfo import available_timezones -from flask import Response, stream_with_context -from flask_restful import fields # type: ignore - from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.file import helpers as file_helpers from extensions.ext_redis import redis_client +from flask import Response, stream_with_context +from flask_restful import fields # type: ignore from models.account import Account +from api.configs import dify_config + def run(script): return subprocess.getstatusoutput("source /root/.bashrc && " + script) @@ -297,3 +299,7 @@ def increment_rate_limit(self, email: str): redis_client.zadd(key, {current_time: current_time}) redis_client.expire(key, self.time_window * 2) + + +def get_current_datetime(): + return datetime.now(tz.utc).replace(tzinfo=None) diff --git a/api/models/account.py b/api/models/account.py index 35a28df7505943..e4e1f2ce0769bf 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -265,3 +265,17 @@ class InvitationCode(db.Model): # type: ignore[name-defined] used_by_account_id = db.Column(StringUUID) deprecated_at = db.Column(db.DateTime) created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + + +class AccountDeletionLog(db.Model): + __tablename__ = "account_deletion_logs" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="account_deletion_log_pkey"), + ) + + id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + email = db.Column(db.String(255), nullable=False) + reason = db.Column(db.Text, nullable=True) + + created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py new file mode 100644 index 00000000000000..f149bc12261422 --- /dev/null +++ b/api/services/account_deletion_log_service.py @@ -0,0 +1,34 @@ + +from datetime import timedelta + +from extensions.ext_database import db + +from api.configs import dify_config +from api.libs.helper import get_current_datetime +from api.models.account import AccountDeletionLog + + +class AccountDeletionLogService: + @staticmethod + def create_account_deletion_log(email, reason): + account_deletion_log = AccountDeletionLog() + account_deletion_log.email = email + account_deletion_log.reason = reason + account_deletion_log.updated_at = get_current_datetime() + + return account_deletion_log + + @staticmethod + def email_in_freeze(email): + log = db.session.query(AccountDeletionLog) \ + .filter(AccountDeletionLog.email == email) \ + .order_by(AccountDeletionLog.created_at.desc()) \ + .first() + + if not log: + return False + + # check if email is in freeze + if log.created_at + timedelta(days=dify_config.EMAIL_FREEZE_PERIOD_IN_DAYS) > get_current_datetime(): + return True + return False diff --git a/api/services/account_service.py b/api/services/account_service.py index 2d37db391c899c..23a619d75bff9e 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,51 +8,44 @@ from hashlib import sha256 from typing import Any, Optional, cast -from pydantic import BaseModel -from sqlalchemy import func -from werkzeug.exceptions import Unauthorized - from configs import dify_config from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client -from libs.helper import RateLimiter, TokenManager +from libs.helper import RateLimiter, TokenManager, generate_string from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password from libs.rsa import generate_key_pair -from models.account import ( - Account, - AccountIntegrate, - AccountStatus, - Tenant, - TenantAccountJoin, - TenantAccountJoinRole, - TenantAccountRole, - TenantStatus, -) +from models.account import (Account, AccountIntegrate, AccountStatus, Tenant, + TenantAccountJoin, TenantAccountJoinRole, + TenantAccountRole, TenantStatus) from models.model import DifySetup -from services.errors.account import ( - AccountAlreadyInTenantError, - AccountLoginError, - AccountNotFoundError, - AccountNotLinkTenantError, - AccountPasswordError, - AccountRegisterError, - CannotOperateSelfError, - CurrentPasswordIncorrectError, - InvalidActionError, - LinkAccountIntegrateError, - MemberNotInTenantError, - NoPermissionError, - RoleAlreadyAssignedError, - TenantNotFoundError, -) +from pydantic import BaseModel +from services.errors.account import (AccountAlreadyInTenantError, + AccountLoginError, AccountNotFoundError, + AccountNotLinkTenantError, + AccountPasswordError, + AccountRegisterError, + CannotOperateSelfError, + CurrentPasswordIncorrectError, + InvalidActionError, + LinkAccountIntegrateError, + MemberNotInTenantError, NoPermissionError, + RoleAlreadyAssignedError, + TenantNotFoundError) from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService +from sqlalchemy import func 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_reset_password_task import send_reset_password_mail_task +from werkzeug.exceptions import Unauthorized + +from api.services.account_deletion_log_service import AccountDeletionLogService +from api.tasks.delete_account_task import delete_account_task +from api.tasks.mail_account_deletion_task import \ + send_account_deletion_verification_code class TokenPair(BaseModel): @@ -201,6 +194,10 @@ def create_account( from controllers.console.error import AccountNotFound raise AccountNotFound() + + if AccountDeletionLogService.email_in_freeze(email): + raise AccountRegisterError("Email is in freeze.") + account = Account() account.email = email account.name = name @@ -240,6 +237,35 @@ def create_account_and_tenant( return account + @staticmethod + def generate_account_deletion_verification_code(account: Account) -> tuple[str, str]: + code = generate_string(6) + token = TokenManager.generate_token( + account=account, token_type="account_deletion", additional_data={"code": code} + ) + return code, token + + @staticmethod + def send_account_deletion_verification_email(account: Account, code: str): + language, email = account.interface_language, account.email + send_account_deletion_verification_code.delay(language=language, to=email, code=code) + + @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, reason="") -> None: + """Delete account. This method only adds a task to the queue for deletion.""" + delete_account_task.delay(account, reason) + @staticmethod def link_account_integrate(provider: str, open_id: str, account: Account) -> None: """Link account integrate""" @@ -351,7 +377,8 @@ def send_reset_password_email( raise ValueError("Email must be provided.") if cls.reset_password_rate_limiter.is_rate_limited(account_email): - from controllers.console.auth.error import PasswordResetRateLimitExceededError + from controllers.console.auth.error import \ + PasswordResetRateLimitExceededError raise PasswordResetRateLimitExceededError() @@ -375,6 +402,11 @@ def revoke_reset_password_token(cls, token: str): def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]: return TokenManager.get_token_data(token, "reset_password") + @classmethod + def send_account_delete_verification_email(cls, account: Account, code: str): + language, email = account.interface_language, account.email + send_account_deletion_verification_code.delay(language=language, to=email, code=code) + @classmethod def send_email_code_login_email( cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" @@ -382,7 +414,8 @@ def send_email_code_login_email( if email is None: raise ValueError("Email must be provided.") if cls.email_code_login_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError + from controllers.console.auth.error import \ + EmailCodeLoginRateLimitExceededError raise EmailCodeLoginRateLimitExceededError() diff --git a/api/services/billing_service.py b/api/services/billing_service.py index ed611a8be48679..e8bd1d4a53d2b6 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -2,10 +2,10 @@ from typing import Optional import httpx -from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fixed - from extensions.ext_database import db from models.account import TenantAccountJoin, TenantAccountRole +from tenacity import (retry, retry_if_exception_type, stop_before_delay, + wait_fixed) class BillingService: @@ -70,3 +70,15 @@ def is_tenant_owner_or_admin(current_user): if not TenantAccountRole.is_privileged_role(join.role): raise ValueError("Only team owner or team admin can perform this action") + + @staticmethod + def delete_tenant_customer(cls, tenant_id: str): + """ Delete related customer in billing service. Used when tenant is deleted.""" + params = {"tenant_id": tenant_id} + headers = {"Content-Type": "application/json", "Billing-Api-Secret-Key": cls.secret_key} + + url = f"{cls.base_url}/customer" + response = requests.request("DELETE", url, params=params, headers=headers) + if response.status_code != 200: + raise Exception(f"Failed to delete customer for tenant {tenant_id}.") + return response.json() diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py new file mode 100644 index 00000000000000..ec14aab9d98b83 --- /dev/null +++ b/api/tasks/delete_account_task.py @@ -0,0 +1,67 @@ +import logging +import time + +import click +from celery import shared_task +from extensions.ext_database import db + +from api.models.account import (Account, Tenant, TenantAccountJoin, + TenantAccountJoinRole) +from api.services.account_deletion_log_service import AccountDeletionLogService +from api.services.billing_service import BillingService +from api.tasks.mail_account_deletion_task import (send_deletion_fail_task, + send_deletion_success_task) + +logger = logging.getLogger(__name__) + + +@shared_task(queue="dataset") +def delete_account_task(account: Account, reason: str): + logger.info(click.style("Start delete account task.", fg="green")) + start_at = time.perf_counter() + + logger.info(f"Start deletion of account {account.email}.") + try: + tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() + with db.session.begin(): + # find all tenants this account belongs to + for ta in tenant_account_joins: + if ta.role == TenantAccountJoinRole.OWNER: + # dismiss all members of the tenant + members = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == ta.tenant_id).delete() + logging.info(f"Dismissed {members} members from tenant {ta.tenant_id}.") + + # delete the tenant + db.session.query(Tenant).filter(Tenant.id == ta.tenant_id).delete() + logging.info(f"Deleted tenant {ta.tenant_id}.") + + # delete subscription + try: + BillingService.delete_tenant_customer(ta.tenant_id) + except Exception as e: + logging.error(f"Failed to delete subscription for tenant {ta.tenant_id}: {e}.") + raise + else: + # remove the account from tenant + db.session.delete(ta) + logging.info(f"Removed account {account.email} from tenant {ta.tenant_id}.") + + # delete the account + db.session.delete(account) + + # prepare account deletion log + account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account.email, reason) + db.session.add(account_deletion_log) + + except Exception as e: + logging.error(f"Failed to delete account {account.email}.") + send_deletion_fail_task.delay(account.interface_language, account.email) + return + + send_deletion_success_task.delay(account.interface_language, account.email) + end_at = time.perf_counter() + logging.info( + click.style( + "Account deletion task completed: latency: {}".format(end_at - start_at), fg="green" + ) + ) diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py new file mode 100644 index 00000000000000..561cb5046cbae6 --- /dev/null +++ b/api/tasks/mail_account_deletion_task.py @@ -0,0 +1,127 @@ +import logging +import time + +import click +from celery import shared_task +from extensions.ext_mail import mail +from flask import render_template + + +@shared_task(queue="mail") +def send_deletion_success_task(language, 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: + if language == "zh-Hans": + html_content = render_template( + "delete_account_mail_template_zh-CN.html", + to=to, + # TODO: Add more template variables + ) + mail.send(to=to, subject="Dify 账户删除成功", html=html_content) + else: + html_content = render_template( + "delete_account_mail_template_en-US.html", + to=to, + # TODO: Add more template variables + ) + mail.send(to=to, subject="Dify Account 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_deletion_fail_task(language, to): + """Send email to user regarding account deletion.""" + 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: + if language == "zh-Hans": + html_content = render_template( + "delete_account_fail_mail_template_zh-CN.html", + to=to, + # TODO: Add more template variables + ) + mail.send(to=to, subject="Dify 账户删除失败", html=html_content) + else: + html_content = render_template( + "delete_account_fail_mail_template_en-US.html", + to=to, + # TODO: Add more template variables + ) + mail.send(to=to, subject="Dify Account Deletion Failed", html=html_content) + + end_at = time.perf_counter() + logging.info( + click.style( + "Send account deletion failed 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(language, 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: + if language == "zh-Hans": + html_content = render_template( + "delete_account_verification_code_mail_template_zh-CN.html", + to=to, + code=code + ) + mail.send(to=to, subject="Dify 删除账户验证码", html=html_content) + else: + html_content = render_template( + "delete_account_verification_code_mail_template_en-US.html", + to=to, + code=code + ) + mail.send(to=to, subject="Dify Account Deletion Verification Code", 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)) From cbd8045aba61a8421d16b9707f4158a9e1bb973d Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 18 Dec 2024 23:59:26 -0500 Subject: [PATCH 02/55] fix: use get method for verification code --- api/controllers/console/workspace/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index b256e7c8bb7292..e64c7c111fdb6d 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -249,7 +249,7 @@ class AccountDeleteVerifyApi(Resource): @setup_required @login_required @account_initialization_required - def post(self): + def get(self): account = current_user try: From 4fde7a54df4948e2e8ced61e6d67c076911afee8 Mon Sep 17 00:00:00 2001 From: GareArc Date: Fri, 20 Dec 2024 20:55:08 -0500 Subject: [PATCH 03/55] feat: add rate limiter --- api/controllers/console/auth/error.py | 6 ++++++ api/services/account_service.py | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index e6e30c3c0b015f..8ef10c7bbb11cd 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -53,3 +53,9 @@ class EmailCodeLoginRateLimitExceededError(BaseHTTPException): error_code = "email_code_login_rate_limit_exceeded" description = "Too many login emails have been sent. Please try again in 5 minutes." 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 diff --git a/api/services/account_service.py b/api/services/account_service.py index 23a619d75bff9e..63e07cf10d4718 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -63,6 +63,9 @@ class AccountService: email_code_login_rate_limiter = RateLimiter( 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 @staticmethod @@ -245,11 +248,19 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, ) return code, token - @staticmethod - def send_account_deletion_verification_email(account: Account, code: str): + @classmethod + def send_account_deletion_verification_email(cls, account: Account, code: str): + if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): + from controllers.console.auth.error import \ + EmailCodeAccountDeletionRateLimitExceededError + + raise EmailCodeAccountDeletionRateLimitExceededError() + language, email = account.interface_language, account.email send_account_deletion_verification_code.delay(language=language, 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") From ed2e2600f2c04b6432cda3eae5c4d05891a29926 Mon Sep 17 00:00:00 2001 From: GareArc Date: Fri, 20 Dec 2024 21:37:51 -0500 Subject: [PATCH 04/55] feat: add migration --- api/libs/helper.py | 2 -- ...82e52119c70_added_account_deletion_logs.py | 36 +++++++++++++++++++ api/services/account_deletion_log_service.py | 7 ++-- api/services/account_service.py | 9 +++-- api/tasks/delete_account_task.py | 13 ++++--- 5 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py diff --git a/api/libs/helper.py b/api/libs/helper.py index 8f315078e6a911..d587e980a96bfb 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -21,8 +21,6 @@ from flask_restful import fields # type: ignore from models.account import Account -from api.configs import dify_config - def run(script): return subprocess.getstatusoutput("source /root/.bashrc && " + script) diff --git a/api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py b/api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py new file mode 100644 index 00000000000000..17fd5dbe3a8c0f --- /dev/null +++ b/api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py @@ -0,0 +1,36 @@ +"""added account_deletion_logs + +Revision ID: 582e52119c70 +Revises: e1944c35e15e +Create Date: 2024-12-20 21:36:46.856033 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '582e52119c70' +down_revision = 'e1944c35e15e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('account_deletion_logs', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('reason', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.PrimaryKeyConstraint('id', name='account_deletion_log_pkey') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('account_deletion_logs') + # ### end Alembic commands ### diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py index f149bc12261422..205b16a6b0afe8 100644 --- a/api/services/account_deletion_log_service.py +++ b/api/services/account_deletion_log_service.py @@ -1,11 +1,10 @@ from datetime import timedelta +from configs import dify_config from extensions.ext_database import db - -from api.configs import dify_config -from api.libs.helper import get_current_datetime -from api.models.account import AccountDeletionLog +from libs.helper import get_current_datetime +from models.account import AccountDeletionLog class AccountDeletionLogService: diff --git a/api/services/account_service.py b/api/services/account_service.py index 63e07cf10d4718..4861a03df11c9d 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -22,6 +22,7 @@ TenantAccountRole, TenantStatus) from models.model import DifySetup from pydantic import BaseModel +from services.account_deletion_log_service import AccountDeletionLogService from services.errors.account import (AccountAlreadyInTenantError, AccountLoginError, AccountNotFoundError, AccountNotLinkTenantError, @@ -37,16 +38,14 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService from sqlalchemy import func +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_invite_member_task import send_invite_member_mail_task from tasks.mail_reset_password_task import send_reset_password_mail_task from werkzeug.exceptions import Unauthorized -from api.services.account_deletion_log_service import AccountDeletionLogService -from api.tasks.delete_account_task import delete_account_task -from api.tasks.mail_account_deletion_task import \ - send_account_deletion_verification_code - class TokenPair(BaseModel): access_token: str diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index ec14aab9d98b83..484460fb01e51e 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -4,13 +4,12 @@ import click from celery import shared_task from extensions.ext_database import db - -from api.models.account import (Account, Tenant, TenantAccountJoin, - TenantAccountJoinRole) -from api.services.account_deletion_log_service import AccountDeletionLogService -from api.services.billing_service import BillingService -from api.tasks.mail_account_deletion_task import (send_deletion_fail_task, - send_deletion_success_task) +from models.account import (Account, Tenant, TenantAccountJoin, + TenantAccountJoinRole) +from services.account_deletion_log_service import AccountDeletionLogService +from services.billing_service import BillingService +from tasks.mail_account_deletion_task import (send_deletion_fail_task, + send_deletion_success_task) logger = logging.getLogger(__name__) From b7e8e746017d6e85c67501f9df872810b22d0d78 Mon Sep 17 00:00:00 2001 From: GareArc Date: Sat, 21 Dec 2024 14:05:33 -0500 Subject: [PATCH 05/55] update --- api/configs/feature/__init__.py | 8 ++++++++ api/tasks/delete_account_task.py | 28 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 0f6a62ac73c262..1dd36077b4115d 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -764,6 +764,13 @@ class RegisterConfig(BaseSettings): ) +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( # place the configs in alphabet order AppExecutionConfig, @@ -792,6 +799,7 @@ class FeatureConfig( WorkspaceConfig, LoginConfig, RegisterConfig, + AccountConfig, # hosted services config HostedServiceConfig, CeleryBeatConfig, diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 484460fb01e51e..c72d88fd404d2d 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -20,11 +20,11 @@ def delete_account_task(account: Account, reason: str): start_at = time.perf_counter() logger.info(f"Start deletion of account {account.email}.") - try: - tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() - with db.session.begin(): - # find all tenants this account belongs to - for ta in tenant_account_joins: + # find all tenants this account belongs to + tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() + for ta in tenant_account_joins: + try: + with db.session.begin(): if ta.role == TenantAccountJoinRole.OWNER: # dismiss all members of the tenant members = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == ta.tenant_id).delete() @@ -45,17 +45,17 @@ def delete_account_task(account: Account, reason: str): db.session.delete(ta) logging.info(f"Removed account {account.email} from tenant {ta.tenant_id}.") - # delete the account - db.session.delete(account) + # delete the account + db.session.delete(account) - # prepare account deletion log - account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account.email, reason) - db.session.add(account_deletion_log) + # prepare account deletion log + account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account.email, reason) + db.session.add(account_deletion_log) - except Exception as e: - logging.error(f"Failed to delete account {account.email}.") - send_deletion_fail_task.delay(account.interface_language, account.email) - return + except Exception as e: + logging.error(f"Failed to delete account {account.email}.") + send_deletion_fail_task.delay(account.interface_language, account.email) + return send_deletion_success_task.delay(account.interface_language, account.email) end_at = time.perf_counter() From 5e25799550836d1aed95242812a820aeb923a414 Mon Sep 17 00:00:00 2001 From: GareArc Date: Sat, 21 Dec 2024 14:13:19 -0500 Subject: [PATCH 06/55] fix: token wrong position --- api/services/account_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 4861a03df11c9d..70ecb16213c104 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -245,7 +245,8 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, token = TokenManager.generate_token( account=account, token_type="account_deletion", additional_data={"code": code} ) - return code, token + logging.info(f"Account {account.id} generated account deletion verification code {code} with token {token}") + return token, code @classmethod def send_account_deletion_verification_email(cls, account: Account, code: str): From ff5b846c51dbab25ab32217fa3171b419e59d730 Mon Sep 17 00:00:00 2001 From: GareArc Date: Sat, 21 Dec 2024 14:20:40 -0500 Subject: [PATCH 07/55] fix: params of celery function should be serializable --- api/services/account_service.py | 2 +- api/tasks/delete_account_task.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 70ecb16213c104..ba14e075260e81 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -275,7 +275,7 @@ def verify_account_deletion_code(token: str, code: str) -> bool: @staticmethod def delete_account(account: Account, reason="") -> None: """Delete account. This method only adds a task to the queue for deletion.""" - delete_account_task.delay(account, reason) + delete_account_task.delay(account.id, reason) @staticmethod def link_account_integrate(provider: str, open_id: str, account: Account) -> None: diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index c72d88fd404d2d..ffea9d971e6491 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -15,7 +15,8 @@ @shared_task(queue="dataset") -def delete_account_task(account: Account, reason: str): +def delete_account_task(account_id, reason: str): + account = db.session.query(Account).filter(Account.id == account_id).first() logger.info(click.style("Start delete account task.", fg="green")) start_at = time.perf_counter() From 7b49e740a6d91a597353d4552d37e29a94cf668d Mon Sep 17 00:00:00 2001 From: GareArc Date: Sat, 21 Dec 2024 15:03:16 -0500 Subject: [PATCH 08/55] fix: db session error --- api/controllers/console/workspace/account.py | 5 +- api/services/account_service.py | 1 - api/tasks/delete_account_task.py | 50 ++++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index e64c7c111fdb6d..ee2065aa18727f 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -23,7 +23,6 @@ from services.account_service import AccountService from services.errors.account import \ CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError -from services.errors.account import RateLimitExceededError class AccountInitApi(Resource): @@ -255,8 +254,8 @@ def get(self): try: token, code = AccountService.generate_account_deletion_verification_code(account) AccountService.send_account_delete_verification_email(account, code) - except RateLimitExceededError: - return {"result": "fail", "error": "Rate limit exceeded."}, 429 + except Exception as e: + return {"result": "fail", "error": str(e)}, 429 return {"result": "success", "data": token} diff --git a/api/services/account_service.py b/api/services/account_service.py index ba14e075260e81..c365729bd7f801 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -245,7 +245,6 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, token = TokenManager.generate_token( account=account, token_type="account_deletion", additional_data={"code": code} ) - logging.info(f"Account {account.id} generated account deletion verification code {code} with token {token}") return token, code @classmethod diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index ffea9d971e6491..bc13830f3a86ab 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -25,36 +25,38 @@ def delete_account_task(account_id, reason: str): tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() for ta in tenant_account_joins: try: - with db.session.begin(): - if ta.role == TenantAccountJoinRole.OWNER: - # dismiss all members of the tenant - members = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == ta.tenant_id).delete() - logging.info(f"Dismissed {members} members from tenant {ta.tenant_id}.") + if ta.role == TenantAccountJoinRole.OWNER: + # dismiss all members of the tenant + members = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == ta.tenant_id).delete() + logging.info(f"Dismissed {members} members from tenant {ta.tenant_id}.") - # delete the tenant - db.session.query(Tenant).filter(Tenant.id == ta.tenant_id).delete() - logging.info(f"Deleted tenant {ta.tenant_id}.") + # delete the tenant + db.session.query(Tenant).filter(Tenant.id == ta.tenant_id).delete() + logging.info(f"Deleted tenant {ta.tenant_id}.") - # delete subscription - try: - BillingService.delete_tenant_customer(ta.tenant_id) - except Exception as e: - logging.error(f"Failed to delete subscription for tenant {ta.tenant_id}: {e}.") - raise - else: - # remove the account from tenant - db.session.delete(ta) - logging.info(f"Removed account {account.email} from tenant {ta.tenant_id}.") + # delete subscription + try: + BillingService.delete_tenant_customer(ta.tenant_id) + except Exception as e: + logging.error(f"Failed to delete subscription for tenant {ta.tenant_id}: {e}.") + raise + else: + # remove the account from tenant + db.session.delete(ta) + logging.info(f"Removed account {account.email} from tenant {ta.tenant_id}.") - # delete the account - db.session.delete(account) + # delete the account + db.session.delete(account) - # prepare account deletion log - account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account.email, reason) - db.session.add(account_deletion_log) + # prepare account deletion log + account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account.email, reason) + db.session.add(account_deletion_log) + + db.session.commit() except Exception as e: - logging.error(f"Failed to delete account {account.email}.") + db.session.rollback() + logging.error(f"Failed to delete account {account.email}: {e}.") send_deletion_fail_task.delay(account.interface_language, account.email) return From e1b0903096816055973135a44005de97050a0662 Mon Sep 17 00:00:00 2001 From: GareArc Date: Sat, 21 Dec 2024 23:11:43 -0500 Subject: [PATCH 09/55] fix: refactor task --- api/tasks/delete_account_task.py | 114 ++++++++++++++++++------------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index bc13830f3a86ab..b6599b7395ddc9 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -17,53 +17,75 @@ @shared_task(queue="dataset") def delete_account_task(account_id, reason: str): account = db.session.query(Account).filter(Account.id == account_id).first() - logger.info(click.style("Start delete account task.", fg="green")) + if not account: + logging.error(f"Account with ID {account_id} not found.") + return + + logger.info(click.style(f"Start deletion task for account {account.email}.", fg="green")) start_at = time.perf_counter() - logger.info(f"Start deletion of account {account.email}.") - # find all tenants this account belongs to - tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() - for ta in tenant_account_joins: - try: - if ta.role == TenantAccountJoinRole.OWNER: - # dismiss all members of the tenant - members = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == ta.tenant_id).delete() - logging.info(f"Dismissed {members} members from tenant {ta.tenant_id}.") - - # delete the tenant - db.session.query(Tenant).filter(Tenant.id == ta.tenant_id).delete() - logging.info(f"Deleted tenant {ta.tenant_id}.") - - # delete subscription - try: - BillingService.delete_tenant_customer(ta.tenant_id) - except Exception as e: - logging.error(f"Failed to delete subscription for tenant {ta.tenant_id}: {e}.") - raise - else: - # remove the account from tenant - db.session.delete(ta) - logging.info(f"Removed account {account.email} from tenant {ta.tenant_id}.") - - # delete the account - db.session.delete(account) - - # prepare account deletion log - account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account.email, reason) - db.session.add(account_deletion_log) - - db.session.commit() - - except Exception as e: - db.session.rollback() - logging.error(f"Failed to delete account {account.email}: {e}.") - send_deletion_fail_task.delay(account.interface_language, account.email) - return - - send_deletion_success_task.delay(account.interface_language, account.email) - end_at = time.perf_counter() - logging.info( - click.style( - "Account deletion task completed: latency: {}".format(end_at - start_at), fg="green" + try: + _process_account_deletion(account, reason) + db.session.commit() + send_deletion_success_task.delay(account.interface_language, account.email) + logger.info( + click.style( + f"Account deletion task completed for {account.email}: latency: {time.perf_counter() - start_at}", + fg="green", + ) ) + except Exception as e: + db.session.rollback() + logging.error(f"Failed to delete account {account.email}: {e}.") + send_deletion_fail_task.delay(account.interface_language, account.email) + raise + + +def _process_account_deletion(account, reason): + # Fetch all tenant-account associations + tenant_account_joins = db.session.query(TenantAccountJoin).filter( + TenantAccountJoin.account_id == account.id + ).all() + + for ta in tenant_account_joins: + if ta.role == TenantAccountJoinRole.OWNER: + _handle_owner_tenant_deletion(ta) + else: + _remove_account_from_tenant(ta, account.email) + + account_deletion_log = AccountDeletionLogService.create_account_deletion_log( + account.email, reason ) + db.session.add(account_deletion_log) + db.session.delete(account) + logger.info(f"Account {account.email} successfully deleted.") + + +def _handle_owner_tenant_deletion(ta): + """Handle deletion of a tenant where the account is an owner.""" + tenant_id = ta.tenant_id + + # Dismiss all tenant members + members_deleted = db.session.query(TenantAccountJoin).filter( + TenantAccountJoin.tenant_id == tenant_id + ).delete() + logger.info(f"Dismissed {members_deleted} members from tenant {tenant_id}.") + + # Delete the tenant + db.session.query(Tenant).filter(Tenant.id == tenant_id).delete() + logger.info(f"Deleted tenant {tenant_id}.") + + # Delete subscription + try: + BillingService.delete_tenant_customer(tenant_id) + logger.info(f"Subscription for tenant {tenant_id} deleted successfully.") + except Exception as e: + logger.error(f"Failed to delete subscription for tenant {tenant_id}: {e}.") + raise + + +def _remove_account_from_tenant(ta, email): + """Remove the account from a tenant.""" + tenant_id = ta.tenant_id + db.session.delete(ta) + logger.info(f"Removed account {email} from tenant {tenant_id}.") From d31161d84833ecfbf521fcbe77351693927e70de Mon Sep 17 00:00:00 2001 From: GareArc Date: Sun, 22 Dec 2024 05:03:37 +0000 Subject: [PATCH 10/55] minor fix --- api/services/billing_service.py | 2 +- api/tasks/delete_account_task.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index e8bd1d4a53d2b6..6d2b88e602e21c 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -71,7 +71,7 @@ def is_tenant_owner_or_admin(current_user): if not TenantAccountRole.is_privileged_role(join.role): raise ValueError("Only team owner or team admin can perform this action") - @staticmethod + @classmethod def delete_tenant_customer(cls, tenant_id: str): """ Delete related customer in billing service. Used when tenant is deleted.""" params = {"tenant_id": tenant_id} diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index b6599b7395ddc9..6f746177ed8546 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -48,7 +48,7 @@ def _process_account_deletion(account, reason): ).all() for ta in tenant_account_joins: - if ta.role == TenantAccountJoinRole.OWNER: + if ta.role == TenantAccountJoinRole.OWNER.value: _handle_owner_tenant_deletion(ta) else: _remove_account_from_tenant(ta, account.email) From 0dc870a3ba1b5299b329701b9b5b180ade8846e7 Mon Sep 17 00:00:00 2001 From: NFish Date: Mon, 23 Dec 2024 10:39:43 +0800 Subject: [PATCH 11/55] feat: add email templates --- ...ete_account_code_email_template_en-US.html | 74 +++++++++++++++++++ .../delete_account_code_email_zh-CN.html | 74 +++++++++++++++++++ .../delete_account_fail_template_en-US.html | 70 ++++++++++++++++++ .../delete_account_fail_template_zh-CN.html | 70 ++++++++++++++++++ ...delete_account_success_template_en-US.html | 70 ++++++++++++++++++ ...delete_account_success_template_zh-CN.html | 70 ++++++++++++++++++ 6 files changed, 428 insertions(+) create mode 100644 api/templates/delete_account_code_email_template_en-US.html create mode 100644 api/templates/delete_account_code_email_zh-CN.html create mode 100644 api/templates/delete_account_fail_template_en-US.html create mode 100644 api/templates/delete_account_fail_template_zh-CN.html create mode 100644 api/templates/delete_account_success_template_en-US.html create mode 100644 api/templates/delete_account_success_template_zh-CN.html diff --git a/api/templates/delete_account_code_email_template_en-US.html b/api/templates/delete_account_code_email_template_en-US.html new file mode 100644 index 00000000000000..5a1649402cefcb --- /dev/null +++ b/api/templates/delete_account_code_email_template_en-US.html @@ -0,0 +1,74 @@ + + + + + + +
+
+ + Dify Logo +
+

Dify 的删除账户验证码

+

复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。

+
+ {{code}} +
+

如果您没有请求删除账户,请不要担心。您可以安全地忽略此电子邮件。

+
+ + diff --git a/api/templates/delete_account_code_email_zh-CN.html b/api/templates/delete_account_code_email_zh-CN.html new file mode 100644 index 00000000000000..b7f236be3c2da1 --- /dev/null +++ b/api/templates/delete_account_code_email_zh-CN.html @@ -0,0 +1,74 @@ + + + + + + +
+
+ + Dify Logo +
+

Delete your Dify account

+

Copy and paste this code, this code will only be valid for the next 5 minutes.

+
+ {{code}} +
+

If you didn't request, don't worry. You can safely ignore this email.

+
+ + diff --git a/api/templates/delete_account_fail_template_en-US.html b/api/templates/delete_account_fail_template_en-US.html new file mode 100644 index 00000000000000..84a724c582b8e8 --- /dev/null +++ b/api/templates/delete_account_fail_template_en-US.html @@ -0,0 +1,70 @@ + + + + + + +
+
+ + Dify Logo +
+

Ops! Your account delete failed

+

You recently deleted your {{email}} Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

+
+ + diff --git a/api/templates/delete_account_fail_template_zh-CN.html b/api/templates/delete_account_fail_template_zh-CN.html new file mode 100644 index 00000000000000..e9a3234c121d72 --- /dev/null +++ b/api/templates/delete_account_fail_template_zh-CN.html @@ -0,0 +1,70 @@ + + + + + + +
+
+ + Dify Logo +
+

账户删除失败

+

You recently deleted your {{email}} Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

+
+ + diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html new file mode 100644 index 00000000000000..30e592bdc1b9fa --- /dev/null +++ b/api/templates/delete_account_success_template_en-US.html @@ -0,0 +1,70 @@ + + + + + + +
+
+ + Dify Logo +
+

Your account has finished deleting

+

You recently deleted your {{email}} Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

+
+ + diff --git a/api/templates/delete_account_success_template_zh-CN.html b/api/templates/delete_account_success_template_zh-CN.html new file mode 100644 index 00000000000000..622c1d90f07a8a --- /dev/null +++ b/api/templates/delete_account_success_template_zh-CN.html @@ -0,0 +1,70 @@ + + + + + + +
+
+ + Dify Logo +
+

您的账户已删除

+

您最近删除了您的 {{email}} Dify 账户。该操作已完成,账户现已删除。请注意,您将不再能够访问此账户中曾经加入的所有工作区。

+
+ + From 341cc22d1f6e843c69005c73a3e2500c12ba4288 Mon Sep 17 00:00:00 2001 From: NFish Date: Mon, 23 Dec 2024 11:29:04 +0800 Subject: [PATCH 12/55] fix: update emial template style --- .../delete_account_success_template_en-US.html | 16 +++++++++++----- .../delete_account_success_template_zh-CN.html | 15 +++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html index 30e592bdc1b9fa..d10a913da685d2 100644 --- a/api/templates/delete_account_success_template_en-US.html +++ b/api/templates/delete_account_success_template_en-US.html @@ -33,10 +33,10 @@ line-height: 28.8px; } .description { - font-size: 13px; - line-height: 16px; - color: #676f83; - margin-top: 12px; + color: #354052; + font-weight: 400; + line-height: 20px; + font-size: 14px; } .code-content { padding: 16px 32px; @@ -55,6 +55,12 @@ color: #676f83; font-size: 13px; } + .email{ + color: #354052; + font-weight: 600; + line-height: 20px; + font-size: 14px; + } @@ -64,7 +70,7 @@ Dify Logo

Your account has finished deleting

-

You recently deleted your {{email}} Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

+

You recently deleted your Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

diff --git a/api/templates/delete_account_success_template_zh-CN.html b/api/templates/delete_account_success_template_zh-CN.html index 622c1d90f07a8a..7381dba81c19d1 100644 --- a/api/templates/delete_account_success_template_zh-CN.html +++ b/api/templates/delete_account_success_template_zh-CN.html @@ -33,9 +33,10 @@ line-height: 28.8px; } .description { - font-size: 13px; - line-height: 16px; - color: #676f83; + color: #354052; + font-weight: 400; + line-height: 20px; + font-size: 14px; margin-top: 12px; } .code-content { @@ -55,6 +56,12 @@ color: #676f83; font-size: 13px; } + .email{ + color: #354052; + font-weight: 600; + line-height: 20px; + font-size: 14px; + } @@ -64,7 +71,7 @@ Dify Logo

您的账户已删除

-

您最近删除了您的 {{email}} Dify 账户。该操作已完成,账户现已删除。请注意,您将不再能够访问此账户中曾经加入的所有工作区。

+

您最近删除了您的 Dify 账户 。系统已完成处理,现在此账户已被删除。请注意,您不再有权访问此帐户曾经所在的所有空间。

From c016c447b4ba0b80885349245e70bc09a90ed8d9 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 00:28:49 -0500 Subject: [PATCH 13/55] fix: add detailed deletion log --- api/libs/helper.py | 42 ++++++++++- api/models/account.py | 18 +++++ api/services/account_deletion_log_service.py | 10 +-- api/services/billing_service.py | 10 +-- api/tasks/delete_account_task.py | 49 +++++++++---- api/tasks/mail_account_deletion_task.py | 53 +++----------- ...l => delete_account_code_email_en-US.html} | 0 ...te_account_code_email_template_zh-CN.html} | 0 .../delete_account_fail_template_en-US.html | 70 ------------------- .../delete_account_fail_template_zh-CN.html | 70 ------------------- 10 files changed, 109 insertions(+), 213 deletions(-) rename api/templates/{delete_account_code_email_zh-CN.html => delete_account_code_email_en-US.html} (100%) rename api/templates/{delete_account_code_email_template_en-US.html => delete_account_code_email_template_zh-CN.html} (100%) delete mode 100644 api/templates/delete_account_fail_template_en-US.html delete mode 100644 api/templates/delete_account_fail_template_zh-CN.html diff --git a/api/libs/helper.py b/api/libs/helper.py index d587e980a96bfb..e17a924ad02eeb 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -7,7 +7,7 @@ import time import uuid from collections.abc import Generator, Mapping -from datetime import datetime +from datetime import date, datetime from datetime import timezone as tz from hashlib import sha256 from typing import Any, Optional, Union, cast @@ -301,3 +301,43 @@ def increment_rate_limit(self, email: str): def get_current_datetime(): return datetime.now(tz.utc).replace(tzinfo=None) + + +def to_json(obj): + """ + Convert a Python object to a compact JSON string. + + Supports basic types, datetime, UUID, SQLAlchemy models, and nested structures. + + Args: + obj: The object to convert. + + Returns: + A compact JSON string representation of the object. + """ + def serialize(obj, seen=None): + if seen is None: + seen = set() # Track seen objects to prevent recursion + + if id(obj) in seen: + return None # Avoid circular references + + seen.add(id(obj)) + + if isinstance(obj, (datetime, date)): + return obj.isoformat() # Convert datetime and date to ISO 8601 string + elif isinstance(obj, uuid.UUID): + return str(obj) # Convert UUID to string + elif isinstance(obj, (int, float, str, bool, type(None))): + return obj # Leave basic types unchanged + elif isinstance(obj, dict): + return {key: serialize(value, seen) for key, value in obj.items() if value is not None} # Exclude None values + elif isinstance(obj, list): + return [serialize(item, seen) for item in obj] # Recursively serialize lists + else: + try: + return str(obj) # Fallback to string for unsupported types + except Exception: + return None # Return None if serialization fails + + return json.dumps(serialize(obj), separators=(',', ':')) diff --git a/api/models/account.py b/api/models/account.py index e4e1f2ce0769bf..355f5cb48a25f6 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -276,6 +276,24 @@ class AccountDeletionLog(db.Model): id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) email = db.Column(db.String(255), nullable=False) reason = db.Column(db.Text, nullable=True) + account_id = db.Column(StringUUID, nullable=False) + snapshot = db.Column(db.Text, nullable=False) + + created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + + +class AccountDeletionLogDetail(db.Model): + __tablename__ = "account_deletion_log_details" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="account_deletion_log_detail_pkey"), + ) + + id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + account_id = db.Column(StringUUID, nullable=False) + tenant_id = db.Column(StringUUID, nullable=False) + role = db.Column(db.String(16), nullable=False) + snapshot = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py index 205b16a6b0afe8..2dc729c829c3e9 100644 --- a/api/services/account_deletion_log_service.py +++ b/api/services/account_deletion_log_service.py @@ -3,16 +3,18 @@ from configs import dify_config from extensions.ext_database import db -from libs.helper import get_current_datetime -from models.account import AccountDeletionLog +from libs.helper import get_current_datetime, to_json +from models.account import Account, AccountDeletionLog class AccountDeletionLogService: @staticmethod - def create_account_deletion_log(email, reason): + def create_account_deletion_log(account: Account, reason): account_deletion_log = AccountDeletionLog() - account_deletion_log.email = email + account_deletion_log.email = account.email account_deletion_log.reason = reason + account_deletion_log.account_id = account.id + account_deletion_log.snapshot = to_json(account) account_deletion_log.updated_at = get_current_datetime() return account_deletion_log diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 6d2b88e602e21c..ee3dafc04949b6 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -72,13 +72,7 @@ def is_tenant_owner_or_admin(current_user): raise ValueError("Only team owner or team admin can perform this action") @classmethod - def delete_tenant_customer(cls, tenant_id: str): + def unsubscripbe_tenant_customer(cls, tenant_id: str): """ Delete related customer in billing service. Used when tenant is deleted.""" params = {"tenant_id": tenant_id} - headers = {"Content-Type": "application/json", "Billing-Api-Secret-Key": cls.secret_key} - - url = f"{cls.base_url}/customer" - response = requests.request("DELETE", url, params=params, headers=headers) - if response.status_code != 200: - raise Exception(f"Failed to delete customer for tenant {tenant_id}.") - return response.json() + return cls._send_request("DELETE", "/subscription", params=params) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 6f746177ed8546..2a702c39180f1f 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -4,12 +4,13 @@ import click from celery import shared_task from extensions.ext_database import db -from models.account import (Account, Tenant, TenantAccountJoin, - TenantAccountJoinRole) +from models.account import (Account, AccountDeletionLogDetail, + TenantAccountJoin, TenantAccountJoinRole) from services.account_deletion_log_service import AccountDeletionLogService from services.billing_service import BillingService -from tasks.mail_account_deletion_task import (send_deletion_fail_task, - send_deletion_success_task) +from tasks.mail_account_deletion_task import send_deletion_success_task + +from api.libs.helper import to_json logger = logging.getLogger(__name__) @@ -37,7 +38,6 @@ def delete_account_task(account_id, reason: str): except Exception as e: db.session.rollback() logging.error(f"Failed to delete account {account.email}: {e}.") - send_deletion_fail_task.delay(account.interface_language, account.email) raise @@ -54,38 +54,57 @@ def _process_account_deletion(account, reason): _remove_account_from_tenant(ta, account.email) account_deletion_log = AccountDeletionLogService.create_account_deletion_log( - account.email, reason + account, reason ) db.session.add(account_deletion_log) db.session.delete(account) logger.info(f"Account {account.email} successfully deleted.") -def _handle_owner_tenant_deletion(ta): +def _handle_owner_tenant_deletion(ta: TenantAccountJoin): """Handle deletion of a tenant where the account is an owner.""" tenant_id = ta.tenant_id # Dismiss all tenant members - members_deleted = db.session.query(TenantAccountJoin).filter( + members_to_dismiss = db.session.query(TenantAccountJoin).filter( TenantAccountJoin.tenant_id == tenant_id - ).delete() - logger.info(f"Dismissed {members_deleted} members from tenant {tenant_id}.") - - # Delete the tenant - db.session.query(Tenant).filter(Tenant.id == tenant_id).delete() - logger.info(f"Deleted tenant {tenant_id}.") + ).all() + for member in members_to_dismiss: + db.session.delete(member) + logger.info(f"Dismissed {len(members_to_dismiss)} members from tenant {tenant_id}.") # Delete subscription try: - BillingService.delete_tenant_customer(tenant_id) + BillingService.unsubscripbe_tenant_customer(tenant_id) logger.info(f"Subscription for tenant {tenant_id} deleted successfully.") except Exception as e: logger.error(f"Failed to delete subscription for tenant {tenant_id}: {e}.") raise + # create account deletion log detail + account_deletion_log_detail = AccountDeletionLogDetail() + account_deletion_log_detail.account_id = ta.account_id + account_deletion_log_detail.tenant_id = tenant_id + account_deletion_log_detail.snapshot = to_json({ + "tenant_account_join_info": ta, + "dismissed_members": members_to_dismiss + }) + account_deletion_log_detail.role = ta.role + db.session.add(account_deletion_log_detail) + def _remove_account_from_tenant(ta, email): """Remove the account from a tenant.""" tenant_id = ta.tenant_id db.session.delete(ta) logger.info(f"Removed account {email} from tenant {tenant_id}.") + + # create account deletion log detail + account_deletion_log_detail = AccountDeletionLogDetail() + account_deletion_log_detail.account_id = ta.account_id + account_deletion_log_detail.tenant_id = tenant_id + account_deletion_log_detail.snapshot = to_json({ + "tenant_account_join_info": ta + }) + account_deletion_log_detail.role = ta.role + db.session.add(account_deletion_log_detail) diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py index 561cb5046cbae6..6e65733cf45459 100644 --- a/api/tasks/mail_account_deletion_task.py +++ b/api/tasks/mail_account_deletion_task.py @@ -25,16 +25,16 @@ def send_deletion_success_task(language, to): try: if language == "zh-Hans": html_content = render_template( - "delete_account_mail_template_zh-CN.html", + "delete_account_success_template_zh-CN.html", to=to, - # TODO: Add more template variables + email=to, ) mail.send(to=to, subject="Dify 账户删除成功", html=html_content) else: html_content = render_template( - "delete_account_mail_template_en-US.html", + "delete_account_success_template_en-US.html", to=to, - # TODO: Add more template variables + email=to, ) mail.send(to=to, subject="Dify Account Deleted", html=html_content) @@ -48,43 +48,6 @@ def send_deletion_success_task(language, to): logging.exception("Send account deletion success email to {} failed".format(to)) -@shared_task(queue="mail") -def send_deletion_fail_task(language, to): - """Send email to user regarding account deletion.""" - 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: - if language == "zh-Hans": - html_content = render_template( - "delete_account_fail_mail_template_zh-CN.html", - to=to, - # TODO: Add more template variables - ) - mail.send(to=to, subject="Dify 账户删除失败", html=html_content) - else: - html_content = render_template( - "delete_account_fail_mail_template_en-US.html", - to=to, - # TODO: Add more template variables - ) - mail.send(to=to, subject="Dify Account Deletion Failed", html=html_content) - - end_at = time.perf_counter() - logging.info( - click.style( - "Send account deletion failed 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(language, to, code): """Send email to user regarding account deletion verification code. @@ -104,18 +67,18 @@ def send_account_deletion_verification_code(language, to, code): try: if language == "zh-Hans": html_content = render_template( - "delete_account_verification_code_mail_template_zh-CN.html", + "delete_account_code_email_template_zh-CN.html", to=to, code=code ) - mail.send(to=to, subject="Dify 删除账户验证码", html=html_content) + mail.send(to=to, subject="Dify 的删除账户验证码", html=html_content) else: html_content = render_template( - "delete_account_verification_code_mail_template_en-US.html", + "delete_account_code_email_en-US.html", to=to, code=code ) - mail.send(to=to, subject="Dify Account Deletion Verification Code", html=html_content) + mail.send(to=to, subject="Delete Your Dify Account", html=html_content) end_at = time.perf_counter() logging.info( diff --git a/api/templates/delete_account_code_email_zh-CN.html b/api/templates/delete_account_code_email_en-US.html similarity index 100% rename from api/templates/delete_account_code_email_zh-CN.html rename to api/templates/delete_account_code_email_en-US.html diff --git a/api/templates/delete_account_code_email_template_en-US.html b/api/templates/delete_account_code_email_template_zh-CN.html similarity index 100% rename from api/templates/delete_account_code_email_template_en-US.html rename to api/templates/delete_account_code_email_template_zh-CN.html diff --git a/api/templates/delete_account_fail_template_en-US.html b/api/templates/delete_account_fail_template_en-US.html deleted file mode 100644 index 84a724c582b8e8..00000000000000 --- a/api/templates/delete_account_fail_template_en-US.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - -
-
- - Dify Logo -
-

Ops! Your account delete failed

-

You recently deleted your {{email}} Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

-
- - diff --git a/api/templates/delete_account_fail_template_zh-CN.html b/api/templates/delete_account_fail_template_zh-CN.html deleted file mode 100644 index e9a3234c121d72..00000000000000 --- a/api/templates/delete_account_fail_template_zh-CN.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - -
-
- - Dify Logo -
-

账户删除失败

-

You recently deleted your {{email}} Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

-
- - From 40653f4849849f9d044d1701cfc071d2c96c748d Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 00:48:03 -0500 Subject: [PATCH 14/55] fix: migration --- ...dded_account_deletion_logs_and_details.py} | 21 +++++++++++++++---- api/tasks/delete_account_task.py | 3 +-- 2 files changed, 18 insertions(+), 6 deletions(-) rename api/migrations/versions/{2024_12_20_2136-582e52119c70_added_account_deletion_logs.py => 2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py} (50%) diff --git a/api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py b/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py similarity index 50% rename from api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py rename to api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py index 17fd5dbe3a8c0f..02b49b21be3455 100644 --- a/api/migrations/versions/2024_12_20_2136-582e52119c70_added_account_deletion_logs.py +++ b/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py @@ -1,8 +1,8 @@ -"""added account_deletion_logs +"""added account_deletion_logs and details -Revision ID: 582e52119c70 +Revision ID: 35e1a3223204 Revises: e1944c35e15e -Create Date: 2024-12-20 21:36:46.856033 +Create Date: 2024-12-23 00:47:44.483419 """ from alembic import op @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = '582e52119c70' +revision = '35e1a3223204' down_revision = 'e1944c35e15e' branch_labels = None depends_on = None @@ -19,10 +19,22 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.create_table('account_deletion_log_details', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('account_id', models.types.StringUUID(), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('role', sa.String(length=16), nullable=False), + sa.Column('snapshot', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.PrimaryKeyConstraint('id', name='account_deletion_log_detail_pkey') + ) op.create_table('account_deletion_logs', sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), sa.Column('email', sa.String(length=255), nullable=False), sa.Column('reason', sa.Text(), nullable=True), + sa.Column('account_id', models.types.StringUUID(), nullable=False), + sa.Column('snapshot', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), sa.PrimaryKeyConstraint('id', name='account_deletion_log_pkey') @@ -33,4 +45,5 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_table('account_deletion_logs') + op.drop_table('account_deletion_log_details') # ### end Alembic commands ### diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 2a702c39180f1f..5850183245fa0e 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -4,14 +4,13 @@ import click from celery import shared_task from extensions.ext_database import db +from libs.helper import to_json from models.account import (Account, AccountDeletionLogDetail, TenantAccountJoin, TenantAccountJoinRole) from services.account_deletion_log_service import AccountDeletionLogService from services.billing_service import BillingService from tasks.mail_account_deletion_task import send_deletion_success_task -from api.libs.helper import to_json - logger = logging.getLogger(__name__) From bf7d30f0e1c9fa1ff4d1cc33427e62030475bdc7 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 01:17:32 -0500 Subject: [PATCH 15/55] fix: remove custom json serializer --- api/libs/helper.py | 42 +------------------- api/services/account_deletion_log_service.py | 5 ++- api/tasks/delete_account_task.py | 6 +-- 3 files changed, 7 insertions(+), 46 deletions(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index e17a924ad02eeb..d587e980a96bfb 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -7,7 +7,7 @@ import time import uuid from collections.abc import Generator, Mapping -from datetime import date, datetime +from datetime import datetime from datetime import timezone as tz from hashlib import sha256 from typing import Any, Optional, Union, cast @@ -301,43 +301,3 @@ def increment_rate_limit(self, email: str): def get_current_datetime(): return datetime.now(tz.utc).replace(tzinfo=None) - - -def to_json(obj): - """ - Convert a Python object to a compact JSON string. - - Supports basic types, datetime, UUID, SQLAlchemy models, and nested structures. - - Args: - obj: The object to convert. - - Returns: - A compact JSON string representation of the object. - """ - def serialize(obj, seen=None): - if seen is None: - seen = set() # Track seen objects to prevent recursion - - if id(obj) in seen: - return None # Avoid circular references - - seen.add(id(obj)) - - if isinstance(obj, (datetime, date)): - return obj.isoformat() # Convert datetime and date to ISO 8601 string - elif isinstance(obj, uuid.UUID): - return str(obj) # Convert UUID to string - elif isinstance(obj, (int, float, str, bool, type(None))): - return obj # Leave basic types unchanged - elif isinstance(obj, dict): - return {key: serialize(value, seen) for key, value in obj.items() if value is not None} # Exclude None values - elif isinstance(obj, list): - return [serialize(item, seen) for item in obj] # Recursively serialize lists - else: - try: - return str(obj) # Fallback to string for unsupported types - except Exception: - return None # Return None if serialization fails - - return json.dumps(serialize(obj), separators=(',', ':')) diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py index 2dc729c829c3e9..19c385ed41ee99 100644 --- a/api/services/account_deletion_log_service.py +++ b/api/services/account_deletion_log_service.py @@ -3,7 +3,8 @@ from configs import dify_config from extensions.ext_database import db -from libs.helper import get_current_datetime, to_json +from flask import jsonify +from libs.helper import get_current_datetime from models.account import Account, AccountDeletionLog @@ -14,7 +15,7 @@ def create_account_deletion_log(account: Account, reason): account_deletion_log.email = account.email account_deletion_log.reason = reason account_deletion_log.account_id = account.id - account_deletion_log.snapshot = to_json(account) + account_deletion_log.snapshot = jsonify(account) account_deletion_log.updated_at = get_current_datetime() return account_deletion_log diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 5850183245fa0e..ab997dba5d6599 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -4,7 +4,7 @@ import click from celery import shared_task from extensions.ext_database import db -from libs.helper import to_json +from flask import jsonify from models.account import (Account, AccountDeletionLogDetail, TenantAccountJoin, TenantAccountJoinRole) from services.account_deletion_log_service import AccountDeletionLogService @@ -84,7 +84,7 @@ def _handle_owner_tenant_deletion(ta: TenantAccountJoin): account_deletion_log_detail = AccountDeletionLogDetail() account_deletion_log_detail.account_id = ta.account_id account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = to_json({ + account_deletion_log_detail.snapshot = jsonify({ "tenant_account_join_info": ta, "dismissed_members": members_to_dismiss }) @@ -102,7 +102,7 @@ def _remove_account_from_tenant(ta, email): account_deletion_log_detail = AccountDeletionLogDetail() account_deletion_log_detail.account_id = ta.account_id account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = to_json({ + account_deletion_log_detail.snapshot = jsonify({ "tenant_account_join_info": ta }) account_deletion_log_detail.role = ta.role From be1b7bc5a9ff2f412f6f67852dc56049d2f63c0c Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 01:39:55 -0500 Subject: [PATCH 16/55] fix: serializer --- api/libs/helper.py | 17 +++++++++++++++++ api/services/account_deletion_log_service.py | 6 +++--- api/tasks/delete_account_task.py | 18 ++++++++++-------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index d587e980a96bfb..bfb1376db9b343 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -301,3 +301,20 @@ def increment_rate_limit(self, email: str): def get_current_datetime(): return datetime.now(tz.utc).replace(tzinfo=None) + + +def serialize_sqlalchemy(obj): + """ + Serializes an SQLAlchemy object into a JSON string. + """ + data = {} + for column in obj.__table__.columns: + value = getattr(obj, column.name) + if isinstance(value, datetime): + data[column.name] = value.isoformat() # ISO 8601 format for datetime + elif isinstance(value, uuid.UUID): + data[column.name] = str(value) # String representation for UUID + else: + data[column.name] = value + + return json.dumps(data, separators=(",", ":")) diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py index 19c385ed41ee99..4a44e3d90fd7f6 100644 --- a/api/services/account_deletion_log_service.py +++ b/api/services/account_deletion_log_service.py @@ -1,10 +1,10 @@ +import json from datetime import timedelta from configs import dify_config from extensions.ext_database import db -from flask import jsonify -from libs.helper import get_current_datetime +from libs.helper import get_current_datetime, serialize_sqlalchemy from models.account import Account, AccountDeletionLog @@ -15,7 +15,7 @@ def create_account_deletion_log(account: Account, reason): account_deletion_log.email = account.email account_deletion_log.reason = reason account_deletion_log.account_id = account.id - account_deletion_log.snapshot = jsonify(account) + account_deletion_log.snapshot = json.dumps(serialize_sqlalchemy(account), separators=(",", ":")) account_deletion_log.updated_at = get_current_datetime() return account_deletion_log diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index ab997dba5d6599..087e48ad5d82b4 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,16 +1,18 @@ +import json import logging import time import click from celery import shared_task from extensions.ext_database import db -from flask import jsonify from models.account import (Account, AccountDeletionLogDetail, TenantAccountJoin, TenantAccountJoinRole) from services.account_deletion_log_service import AccountDeletionLogService from services.billing_service import BillingService from tasks.mail_account_deletion_task import send_deletion_success_task +from api.libs.helper import serialize_sqlalchemy + logger = logging.getLogger(__name__) @@ -84,10 +86,10 @@ def _handle_owner_tenant_deletion(ta: TenantAccountJoin): account_deletion_log_detail = AccountDeletionLogDetail() account_deletion_log_detail.account_id = ta.account_id account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = jsonify({ - "tenant_account_join_info": ta, - "dismissed_members": members_to_dismiss - }) + account_deletion_log_detail.snapshot = json.dumps({ + "tenant_account_join_info": serialize_sqlalchemy(ta), + "dismissed_members": [serialize_sqlalchemy(member) for member in members_to_dismiss] + }, separators=(",", ":")) account_deletion_log_detail.role = ta.role db.session.add(account_deletion_log_detail) @@ -102,8 +104,8 @@ def _remove_account_from_tenant(ta, email): account_deletion_log_detail = AccountDeletionLogDetail() account_deletion_log_detail.account_id = ta.account_id account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = jsonify({ - "tenant_account_join_info": ta - }) + account_deletion_log_detail.snapshot = json.dumps({ + "tenant_account_join_info": serialize_sqlalchemy(ta), + }, separators=(",", ":")) account_deletion_log_detail.role = ta.role db.session.add(account_deletion_log_detail) From bdfc41dda5e1634a329a2b34d085bf43e98b3bb8 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 01:42:29 -0500 Subject: [PATCH 17/55] fix: bad import --- api/tasks/delete_account_task.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 087e48ad5d82b4..2ba20f5a3dae12 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -5,14 +5,13 @@ import click from celery import shared_task from extensions.ext_database import db +from libs.helper import serialize_sqlalchemy from models.account import (Account, AccountDeletionLogDetail, TenantAccountJoin, TenantAccountJoinRole) from services.account_deletion_log_service import AccountDeletionLogService from services.billing_service import BillingService from tasks.mail_account_deletion_task import send_deletion_success_task -from api.libs.helper import serialize_sqlalchemy - logger = logging.getLogger(__name__) From 4254c4d2c8b40f4241e3892811f761ff40d38768 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 01:58:22 -0500 Subject: [PATCH 18/55] fix: add email check in register service --- api/services/account_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/services/account_service.py b/api/services/account_service.py index c365729bd7f801..89311821e240dd 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -844,6 +844,8 @@ def register( ) -> Account: db.session.begin_nested() """Register account""" + if AccountDeletionLogService.email_in_freeze(email): + raise AccountRegisterError("Email is in freeze.") try: account = AccountService.create_account( email=email, From ee52a74bee7fca85fa3dc721bcb3403298ed6624 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 02:19:22 -0500 Subject: [PATCH 19/55] reformat --- api/configs/feature/__init__.py | 16 ++++-- api/controllers/console/workspace/account.py | 2 - api/models/account.py | 8 +-- api/services/account_deletion_log_service.py | 9 +-- api/services/account_service.py | 60 +++++++++++--------- api/services/billing_service.py | 2 +- api/tasks/delete_account_task.py | 40 ++++++------- api/tasks/mail_account_deletion_task.py | 28 ++++----- 8 files changed, 84 insertions(+), 81 deletions(-) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 1dd36077b4115d..ae2f81f7bd6317 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1,11 +1,19 @@ from typing import Annotated, Literal, Optional -from configs.feature.hosted_service import HostedServiceConfig -from pydantic import (AliasChoices, Field, HttpUrl, NegativeInt, - NonNegativeInt, PositiveFloat, PositiveInt, - computed_field) +from pydantic import ( + AliasChoices, + Field, + HttpUrl, + NegativeInt, + NonNegativeInt, + PositiveFloat, + PositiveInt, + computed_field, +) from pydantic_settings import BaseSettings +from configs.feature.hosted_service import HostedServiceConfig + class SecurityConfig(BaseSettings): """ diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index ee2065aa18727f..f10041546be47d 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -244,7 +244,6 @@ def get(self): class AccountDeleteVerifyApi(Resource): - @setup_required @login_required @account_initialization_required @@ -261,7 +260,6 @@ def get(self): class AccountDeleteApi(Resource): - @setup_required @login_required @account_initialization_required diff --git a/api/models/account.py b/api/models/account.py index 355f5cb48a25f6..8c26262b5bb383 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -269,9 +269,7 @@ class InvitationCode(db.Model): # type: ignore[name-defined] class AccountDeletionLog(db.Model): __tablename__ = "account_deletion_logs" - __table_args__ = ( - db.PrimaryKeyConstraint("id", name="account_deletion_log_pkey"), - ) + __table_args__ = (db.PrimaryKeyConstraint("id", name="account_deletion_log_pkey"),) id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) email = db.Column(db.String(255), nullable=False) @@ -285,9 +283,7 @@ class AccountDeletionLog(db.Model): class AccountDeletionLogDetail(db.Model): __tablename__ = "account_deletion_log_details" - __table_args__ = ( - db.PrimaryKeyConstraint("id", name="account_deletion_log_detail_pkey"), - ) + __table_args__ = (db.PrimaryKeyConstraint("id", name="account_deletion_log_detail_pkey"),) id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) account_id = db.Column(StringUUID, nullable=False) diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py index 4a44e3d90fd7f6..1a053bc07739f4 100644 --- a/api/services/account_deletion_log_service.py +++ b/api/services/account_deletion_log_service.py @@ -1,4 +1,3 @@ - import json from datetime import timedelta @@ -22,10 +21,12 @@ def create_account_deletion_log(account: Account, reason): @staticmethod def email_in_freeze(email): - log = db.session.query(AccountDeletionLog) \ - .filter(AccountDeletionLog.email == email) \ - .order_by(AccountDeletionLog.created_at.desc()) \ + log = ( + db.session.query(AccountDeletionLog) + .filter(AccountDeletionLog.email == email) + .order_by(AccountDeletionLog.created_at.desc()) .first() + ) if not log: return False diff --git a/api/services/account_service.py b/api/services/account_service.py index 89311821e240dd..c1bf52762a23e2 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,6 +8,10 @@ from hashlib import sha256 from typing import Any, Optional, cast +from pydantic import BaseModel +from sqlalchemy import func +from werkzeug.exceptions import Unauthorized + from configs import dify_config from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created @@ -17,34 +21,41 @@ from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password from libs.rsa import generate_key_pair -from models.account import (Account, AccountIntegrate, AccountStatus, Tenant, - TenantAccountJoin, TenantAccountJoinRole, - TenantAccountRole, TenantStatus) +from models.account import ( + Account, + AccountIntegrate, + AccountStatus, + Tenant, + TenantAccountJoin, + TenantAccountJoinRole, + TenantAccountRole, + TenantStatus, +) from models.model import DifySetup -from pydantic import BaseModel from services.account_deletion_log_service import AccountDeletionLogService -from services.errors.account import (AccountAlreadyInTenantError, - AccountLoginError, AccountNotFoundError, - AccountNotLinkTenantError, - AccountPasswordError, - AccountRegisterError, - CannotOperateSelfError, - CurrentPasswordIncorrectError, - InvalidActionError, - LinkAccountIntegrateError, - MemberNotInTenantError, NoPermissionError, - RoleAlreadyAssignedError, - TenantNotFoundError) +from services.errors.account import ( + AccountAlreadyInTenantError, + AccountLoginError, + AccountNotFoundError, + AccountNotLinkTenantError, + AccountPasswordError, + AccountRegisterError, + CannotOperateSelfError, + CurrentPasswordIncorrectError, + InvalidActionError, + LinkAccountIntegrateError, + MemberNotInTenantError, + NoPermissionError, + RoleAlreadyAssignedError, + TenantNotFoundError, +) from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService -from sqlalchemy import func 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_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_reset_password_task import send_reset_password_mail_task -from werkzeug.exceptions import Unauthorized class TokenPair(BaseModel): @@ -250,8 +261,7 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, @classmethod def send_account_deletion_verification_email(cls, account: Account, code: str): if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import \ - EmailCodeAccountDeletionRateLimitExceededError + from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError raise EmailCodeAccountDeletionRateLimitExceededError() @@ -387,8 +397,7 @@ def send_reset_password_email( raise ValueError("Email must be provided.") if cls.reset_password_rate_limiter.is_rate_limited(account_email): - from controllers.console.auth.error import \ - PasswordResetRateLimitExceededError + from controllers.console.auth.error import PasswordResetRateLimitExceededError raise PasswordResetRateLimitExceededError() @@ -424,8 +433,7 @@ def send_email_code_login_email( if email is None: raise ValueError("Email must be provided.") if cls.email_code_login_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import \ - EmailCodeLoginRateLimitExceededError + from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError raise EmailCodeLoginRateLimitExceededError() diff --git a/api/services/billing_service.py b/api/services/billing_service.py index ee3dafc04949b6..aa107d36de0281 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -73,6 +73,6 @@ def is_tenant_owner_or_admin(current_user): @classmethod def unsubscripbe_tenant_customer(cls, tenant_id: str): - """ Delete related customer in billing service. Used when tenant is deleted.""" + """Delete related customer in billing service. Used when tenant is deleted.""" params = {"tenant_id": tenant_id} return cls._send_request("DELETE", "/subscription", params=params) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 2ba20f5a3dae12..0db11c7caf0a1a 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -4,10 +4,10 @@ import click from celery import shared_task + from extensions.ext_database import db from libs.helper import serialize_sqlalchemy -from models.account import (Account, AccountDeletionLogDetail, - TenantAccountJoin, TenantAccountJoinRole) +from models.account import Account, AccountDeletionLogDetail, TenantAccountJoin, TenantAccountJoinRole from services.account_deletion_log_service import AccountDeletionLogService from services.billing_service import BillingService from tasks.mail_account_deletion_task import send_deletion_success_task @@ -37,15 +37,13 @@ def delete_account_task(account_id, reason: str): ) except Exception as e: db.session.rollback() - logging.error(f"Failed to delete account {account.email}: {e}.") + logging.exception(f"Failed to delete account {account.email}.") raise def _process_account_deletion(account, reason): # Fetch all tenant-account associations - tenant_account_joins = db.session.query(TenantAccountJoin).filter( - TenantAccountJoin.account_id == account.id - ).all() + tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() for ta in tenant_account_joins: if ta.role == TenantAccountJoinRole.OWNER.value: @@ -53,9 +51,7 @@ def _process_account_deletion(account, reason): else: _remove_account_from_tenant(ta, account.email) - account_deletion_log = AccountDeletionLogService.create_account_deletion_log( - account, reason - ) + account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account, reason) db.session.add(account_deletion_log) db.session.delete(account) logger.info(f"Account {account.email} successfully deleted.") @@ -66,9 +62,7 @@ def _handle_owner_tenant_deletion(ta: TenantAccountJoin): tenant_id = ta.tenant_id # Dismiss all tenant members - members_to_dismiss = db.session.query(TenantAccountJoin).filter( - TenantAccountJoin.tenant_id == tenant_id - ).all() + members_to_dismiss = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == tenant_id).all() for member in members_to_dismiss: db.session.delete(member) logger.info(f"Dismissed {len(members_to_dismiss)} members from tenant {tenant_id}.") @@ -78,17 +72,20 @@ def _handle_owner_tenant_deletion(ta: TenantAccountJoin): BillingService.unsubscripbe_tenant_customer(tenant_id) logger.info(f"Subscription for tenant {tenant_id} deleted successfully.") except Exception as e: - logger.error(f"Failed to delete subscription for tenant {tenant_id}: {e}.") + logger.exception(f"Failed to delete subscription for tenant {tenant_id}.") raise # create account deletion log detail account_deletion_log_detail = AccountDeletionLogDetail() account_deletion_log_detail.account_id = ta.account_id account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = json.dumps({ - "tenant_account_join_info": serialize_sqlalchemy(ta), - "dismissed_members": [serialize_sqlalchemy(member) for member in members_to_dismiss] - }, separators=(",", ":")) + account_deletion_log_detail.snapshot = json.dumps( + { + "tenant_account_join_info": serialize_sqlalchemy(ta), + "dismissed_members": [serialize_sqlalchemy(member) for member in members_to_dismiss], + }, + separators=(",", ":"), + ) account_deletion_log_detail.role = ta.role db.session.add(account_deletion_log_detail) @@ -103,8 +100,11 @@ def _remove_account_from_tenant(ta, email): account_deletion_log_detail = AccountDeletionLogDetail() account_deletion_log_detail.account_id = ta.account_id account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = json.dumps({ - "tenant_account_join_info": serialize_sqlalchemy(ta), - }, separators=(",", ":")) + account_deletion_log_detail.snapshot = json.dumps( + { + "tenant_account_join_info": serialize_sqlalchemy(ta), + }, + separators=(",", ":"), + ) account_deletion_log_detail.role = ta.role db.session.add(account_deletion_log_detail) diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py index 6e65733cf45459..d38df3613bd450 100644 --- a/api/tasks/mail_account_deletion_task.py +++ b/api/tasks/mail_account_deletion_task.py @@ -3,9 +3,10 @@ import click from celery import shared_task -from extensions.ext_mail import mail from flask import render_template +from extensions.ext_mail import mail + @shared_task(queue="mail") def send_deletion_success_task(language, to): @@ -17,9 +18,7 @@ def send_deletion_success_task(language, to): if not mail.is_inited(): return - logging.info( - click.style(f"Start send account deletion success email to {to}", fg="green") - ) + logging.info(click.style(f"Start send account deletion success email to {to}", fg="green")) start_at = time.perf_counter() try: @@ -59,31 +58,24 @@ def send_account_deletion_verification_code(language, to, code): if not mail.is_inited(): return - logging.info( - click.style(f"Start send account deletion verification code email to {to}", fg="green") - ) + logging.info(click.style(f"Start send account deletion verification code email to {to}", fg="green")) start_at = time.perf_counter() try: if language == "zh-Hans": - html_content = render_template( - "delete_account_code_email_template_zh-CN.html", - to=to, - code=code - ) + html_content = render_template("delete_account_code_email_template_zh-CN.html", to=to, code=code) mail.send(to=to, subject="Dify 的删除账户验证码", html=html_content) else: - html_content = render_template( - "delete_account_code_email_en-US.html", - to=to, - code=code - ) + html_content = render_template("delete_account_code_email_en-US.html", to=to, code=code) mail.send(to=to, subject="Delete Your Dify Account", 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" + "Send account deletion verification code email to {} succeeded: latency: {}".format( + to, end_at - start_at + ), + fg="green", ) ) except Exception: From fa7393da17dbb5f44f1a08e31d68e3065b95927f Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 02:51:59 -0500 Subject: [PATCH 20/55] fix: rebase migration head --- ...added_account_deletion_logs_and_details.py | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py b/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py index 02b49b21be3455..d6e210bde83292 100644 --- a/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py +++ b/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py @@ -1,18 +1,17 @@ """added account_deletion_logs and details Revision ID: 35e1a3223204 -Revises: e1944c35e15e +Revises: d7999dfa4aae Create Date: 2024-12-23 00:47:44.483419 """ -from alembic import op import models as models import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision = '35e1a3223204' -down_revision = 'e1944c35e15e' +down_revision = 'd7999dfa4aae' branch_labels = None depends_on = None @@ -20,25 +19,25 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.create_table('account_deletion_log_details', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('account_id', models.types.StringUUID(), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('role', sa.String(length=16), nullable=False), - sa.Column('snapshot', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.PrimaryKeyConstraint('id', name='account_deletion_log_detail_pkey') - ) + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('account_id', models.types.StringUUID(), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('role', sa.String(length=16), nullable=False), + sa.Column('snapshot', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.PrimaryKeyConstraint('id', name='account_deletion_log_detail_pkey') + ) op.create_table('account_deletion_logs', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('reason', sa.Text(), nullable=True), - sa.Column('account_id', models.types.StringUUID(), nullable=False), - sa.Column('snapshot', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.PrimaryKeyConstraint('id', name='account_deletion_log_pkey') - ) + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('reason', sa.Text(), nullable=True), + sa.Column('account_id', models.types.StringUUID(), nullable=False), + sa.Column('snapshot', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.PrimaryKeyConstraint('id', name='account_deletion_log_pkey') + ) # ### end Alembic commands ### From 968ee8c07badf5735b09065ad6ca8e0c5c46901a Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 23:04:22 -0500 Subject: [PATCH 21/55] fix: remove deletion logic --- api/libs/helper.py | 22 ---- api/models/account.py | 28 ------ api/services/account_deletion_log_service.py | 37 ------- api/services/account_service.py | 67 ++++++------- api/services/billing_service.py | 17 +++- api/tasks/delete_account_task.py | 100 ++----------------- 6 files changed, 49 insertions(+), 222 deletions(-) delete mode 100644 api/services/account_deletion_log_service.py diff --git a/api/libs/helper.py b/api/libs/helper.py index bfb1376db9b343..e94ff348d49e27 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -8,7 +8,6 @@ import uuid from collections.abc import Generator, Mapping from datetime import datetime -from datetime import timezone as tz from hashlib import sha256 from typing import Any, Optional, Union, cast from zoneinfo import available_timezones @@ -297,24 +296,3 @@ def increment_rate_limit(self, email: str): redis_client.zadd(key, {current_time: current_time}) redis_client.expire(key, self.time_window * 2) - - -def get_current_datetime(): - return datetime.now(tz.utc).replace(tzinfo=None) - - -def serialize_sqlalchemy(obj): - """ - Serializes an SQLAlchemy object into a JSON string. - """ - data = {} - for column in obj.__table__.columns: - value = getattr(obj, column.name) - if isinstance(value, datetime): - data[column.name] = value.isoformat() # ISO 8601 format for datetime - elif isinstance(value, uuid.UUID): - data[column.name] = str(value) # String representation for UUID - else: - data[column.name] = value - - return json.dumps(data, separators=(",", ":")) diff --git a/api/models/account.py b/api/models/account.py index 8c26262b5bb383..35a28df7505943 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -265,31 +265,3 @@ class InvitationCode(db.Model): # type: ignore[name-defined] used_by_account_id = db.Column(StringUUID) deprecated_at = db.Column(db.DateTime) created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - - -class AccountDeletionLog(db.Model): - __tablename__ = "account_deletion_logs" - __table_args__ = (db.PrimaryKeyConstraint("id", name="account_deletion_log_pkey"),) - - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - email = db.Column(db.String(255), nullable=False) - reason = db.Column(db.Text, nullable=True) - account_id = db.Column(StringUUID, nullable=False) - snapshot = db.Column(db.Text, nullable=False) - - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - - -class AccountDeletionLogDetail(db.Model): - __tablename__ = "account_deletion_log_details" - __table_args__ = (db.PrimaryKeyConstraint("id", name="account_deletion_log_detail_pkey"),) - - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - account_id = db.Column(StringUUID, nullable=False) - tenant_id = db.Column(StringUUID, nullable=False) - role = db.Column(db.String(16), nullable=False) - snapshot = db.Column(db.Text, nullable=False) - - created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) diff --git a/api/services/account_deletion_log_service.py b/api/services/account_deletion_log_service.py deleted file mode 100644 index 1a053bc07739f4..00000000000000 --- a/api/services/account_deletion_log_service.py +++ /dev/null @@ -1,37 +0,0 @@ -import json -from datetime import timedelta - -from configs import dify_config -from extensions.ext_database import db -from libs.helper import get_current_datetime, serialize_sqlalchemy -from models.account import Account, AccountDeletionLog - - -class AccountDeletionLogService: - @staticmethod - def create_account_deletion_log(account: Account, reason): - account_deletion_log = AccountDeletionLog() - account_deletion_log.email = account.email - account_deletion_log.reason = reason - account_deletion_log.account_id = account.id - account_deletion_log.snapshot = json.dumps(serialize_sqlalchemy(account), separators=(",", ":")) - account_deletion_log.updated_at = get_current_datetime() - - return account_deletion_log - - @staticmethod - def email_in_freeze(email): - log = ( - db.session.query(AccountDeletionLog) - .filter(AccountDeletionLog.email == email) - .order_by(AccountDeletionLog.created_at.desc()) - .first() - ) - - if not log: - return False - - # check if email is in freeze - if log.created_at + timedelta(days=dify_config.EMAIL_FREEZE_PERIOD_IN_DAYS) > get_current_datetime(): - return True - return False diff --git a/api/services/account_service.py b/api/services/account_service.py index c1bf52762a23e2..48f68753d9f439 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,10 +8,6 @@ from hashlib import sha256 from typing import Any, Optional, cast -from pydantic import BaseModel -from sqlalchemy import func -from werkzeug.exceptions import Unauthorized - from configs import dify_config from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created @@ -21,41 +17,35 @@ from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password from libs.rsa import generate_key_pair -from models.account import ( - Account, - AccountIntegrate, - AccountStatus, - Tenant, - TenantAccountJoin, - TenantAccountJoinRole, - TenantAccountRole, - TenantStatus, -) +from models.account import (Account, AccountIntegrate, AccountStatus, Tenant, + TenantAccountJoin, TenantAccountJoinRole, + TenantAccountRole, TenantStatus) from models.model import DifySetup -from services.account_deletion_log_service import AccountDeletionLogService -from services.errors.account import ( - AccountAlreadyInTenantError, - AccountLoginError, - AccountNotFoundError, - AccountNotLinkTenantError, - AccountPasswordError, - AccountRegisterError, - CannotOperateSelfError, - CurrentPasswordIncorrectError, - InvalidActionError, - LinkAccountIntegrateError, - MemberNotInTenantError, - NoPermissionError, - RoleAlreadyAssignedError, - TenantNotFoundError, -) +from pydantic import BaseModel +from services.errors.account import (AccountAlreadyInTenantError, + AccountLoginError, AccountNotFoundError, + AccountNotLinkTenantError, + AccountPasswordError, + AccountRegisterError, + CannotOperateSelfError, + CurrentPasswordIncorrectError, + InvalidActionError, + LinkAccountIntegrateError, + MemberNotInTenantError, NoPermissionError, + RoleAlreadyAssignedError, + TenantNotFoundError) from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService +from sqlalchemy import func 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_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_reset_password_task import send_reset_password_mail_task +from werkzeug.exceptions import Unauthorized + +from api.services.billing_service import BillingService class TokenPair(BaseModel): @@ -208,7 +198,7 @@ def create_account( raise AccountNotFound() - if AccountDeletionLogService.email_in_freeze(email): + if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): raise AccountRegisterError("Email is in freeze.") account = Account() @@ -261,7 +251,8 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, @classmethod def send_account_deletion_verification_email(cls, account: Account, code: str): if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError + from controllers.console.auth.error import \ + EmailCodeAccountDeletionRateLimitExceededError raise EmailCodeAccountDeletionRateLimitExceededError() @@ -397,7 +388,8 @@ def send_reset_password_email( raise ValueError("Email must be provided.") if cls.reset_password_rate_limiter.is_rate_limited(account_email): - from controllers.console.auth.error import PasswordResetRateLimitExceededError + from controllers.console.auth.error import \ + PasswordResetRateLimitExceededError raise PasswordResetRateLimitExceededError() @@ -433,7 +425,8 @@ def send_email_code_login_email( if email is None: raise ValueError("Email must be provided.") if cls.email_code_login_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError + from controllers.console.auth.error import \ + EmailCodeLoginRateLimitExceededError raise EmailCodeLoginRateLimitExceededError() @@ -852,7 +845,7 @@ def register( ) -> Account: db.session.begin_nested() """Register account""" - if AccountDeletionLogService.email_in_freeze(email): + if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): raise AccountRegisterError("Email is in freeze.") try: account = AccountService.create_account( diff --git a/api/services/billing_service.py b/api/services/billing_service.py index aa107d36de0281..587cd704eba304 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -72,7 +72,16 @@ def is_tenant_owner_or_admin(current_user): raise ValueError("Only team owner or team admin can perform this action") @classmethod - def unsubscripbe_tenant_customer(cls, tenant_id: str): - """Delete related customer in billing service. Used when tenant is deleted.""" - params = {"tenant_id": tenant_id} - return cls._send_request("DELETE", "/subscription", params=params) + def delete_account(cls, account_id: str, reason: str): + """Delete account.""" + params = {"account_id": account_id, "reason": reason} + 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 response["data"] + except Exception: + return False diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 0db11c7caf0a1a..465567c4ff175c 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,14 +1,8 @@ -import json import logging -import time -import click from celery import shared_task - from extensions.ext_database import db -from libs.helper import serialize_sqlalchemy -from models.account import Account, AccountDeletionLogDetail, TenantAccountJoin, TenantAccountJoinRole -from services.account_deletion_log_service import AccountDeletionLogService +from models.account import Account from services.billing_service import BillingService from tasks.mail_account_deletion_task import send_deletion_success_task @@ -17,94 +11,12 @@ @shared_task(queue="dataset") def delete_account_task(account_id, reason: str): - account = db.session.query(Account).filter(Account.id == account_id).first() - if not account: - logging.error(f"Account with ID {account_id} not found.") - return - - logger.info(click.style(f"Start deletion task for account {account.email}.", fg="green")) - start_at = time.perf_counter() - try: - _process_account_deletion(account, reason) - db.session.commit() - send_deletion_success_task.delay(account.interface_language, account.email) - logger.info( - click.style( - f"Account deletion task completed for {account.email}: latency: {time.perf_counter() - start_at}", - fg="green", - ) - ) + BillingService.delete_account(account_id, reason) except Exception as e: - db.session.rollback() - logging.exception(f"Failed to delete account {account.email}.") + logger.error(f"Failed to delete account {account_id} from billing service: {e}") raise - -def _process_account_deletion(account, reason): - # Fetch all tenant-account associations - tenant_account_joins = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.account_id == account.id).all() - - for ta in tenant_account_joins: - if ta.role == TenantAccountJoinRole.OWNER.value: - _handle_owner_tenant_deletion(ta) - else: - _remove_account_from_tenant(ta, account.email) - - account_deletion_log = AccountDeletionLogService.create_account_deletion_log(account, reason) - db.session.add(account_deletion_log) - db.session.delete(account) - logger.info(f"Account {account.email} successfully deleted.") - - -def _handle_owner_tenant_deletion(ta: TenantAccountJoin): - """Handle deletion of a tenant where the account is an owner.""" - tenant_id = ta.tenant_id - - # Dismiss all tenant members - members_to_dismiss = db.session.query(TenantAccountJoin).filter(TenantAccountJoin.tenant_id == tenant_id).all() - for member in members_to_dismiss: - db.session.delete(member) - logger.info(f"Dismissed {len(members_to_dismiss)} members from tenant {tenant_id}.") - - # Delete subscription - try: - BillingService.unsubscripbe_tenant_customer(tenant_id) - logger.info(f"Subscription for tenant {tenant_id} deleted successfully.") - except Exception as e: - logger.exception(f"Failed to delete subscription for tenant {tenant_id}.") - raise - - # create account deletion log detail - account_deletion_log_detail = AccountDeletionLogDetail() - account_deletion_log_detail.account_id = ta.account_id - account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = json.dumps( - { - "tenant_account_join_info": serialize_sqlalchemy(ta), - "dismissed_members": [serialize_sqlalchemy(member) for member in members_to_dismiss], - }, - separators=(",", ":"), - ) - account_deletion_log_detail.role = ta.role - db.session.add(account_deletion_log_detail) - - -def _remove_account_from_tenant(ta, email): - """Remove the account from a tenant.""" - tenant_id = ta.tenant_id - db.session.delete(ta) - logger.info(f"Removed account {email} from tenant {tenant_id}.") - - # create account deletion log detail - account_deletion_log_detail = AccountDeletionLogDetail() - account_deletion_log_detail.account_id = ta.account_id - account_deletion_log_detail.tenant_id = tenant_id - account_deletion_log_detail.snapshot = json.dumps( - { - "tenant_account_join_info": serialize_sqlalchemy(ta), - }, - separators=(",", ":"), - ) - account_deletion_log_detail.role = ta.role - db.session.add(account_deletion_log_detail) + account = db.session.query(Account).filter(Account.id == account_id).first() + # send success email + send_deletion_success_task.delay(account.interface_language, account.email) From 6caf17f81dbc3a80ab1e9c48900c489c71473ed9 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 23:07:38 -0500 Subject: [PATCH 22/55] fix: delete migration and config --- api/configs/feature/__init__.py | 24 ++-------- ...added_account_deletion_logs_and_details.py | 48 ------------------- 2 files changed, 4 insertions(+), 68 deletions(-) delete mode 100644 api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index ae2f81f7bd6317..3b6e2062aa6d71 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1,18 +1,10 @@ from typing import Annotated, Literal, Optional -from pydantic import ( - AliasChoices, - Field, - HttpUrl, - NegativeInt, - NonNegativeInt, - PositiveFloat, - PositiveInt, - computed_field, -) -from pydantic_settings import BaseSettings - from configs.feature.hosted_service import HostedServiceConfig +from pydantic import (AliasChoices, Field, HttpUrl, NegativeInt, + NonNegativeInt, PositiveFloat, PositiveInt, + computed_field) +from pydantic_settings import BaseSettings class SecurityConfig(BaseSettings): @@ -765,13 +757,6 @@ class LoginConfig(BaseSettings): ) -class RegisterConfig(BaseSettings): - EMAIL_FREEZE_PERIOD_IN_DAYS: PositiveInt = Field( - description="Freeze period in days for re-registering with the same email", - default=30, - ) - - class AccountConfig(BaseSettings): ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field( description="Duration in minutes for which a account deletion token remains valid", @@ -806,7 +791,6 @@ class FeatureConfig( WorkflowNodeExecutionConfig, WorkspaceConfig, LoginConfig, - RegisterConfig, AccountConfig, # hosted services config HostedServiceConfig, diff --git a/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py b/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py deleted file mode 100644 index d6e210bde83292..00000000000000 --- a/api/migrations/versions/2024_12_23_0047-35e1a3223204_added_account_deletion_logs_and_details.py +++ /dev/null @@ -1,48 +0,0 @@ -"""added account_deletion_logs and details - -Revision ID: 35e1a3223204 -Revises: d7999dfa4aae -Create Date: 2024-12-23 00:47:44.483419 - -""" -import models as models -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = '35e1a3223204' -down_revision = 'd7999dfa4aae' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('account_deletion_log_details', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('account_id', models.types.StringUUID(), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('role', sa.String(length=16), nullable=False), - sa.Column('snapshot', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.PrimaryKeyConstraint('id', name='account_deletion_log_detail_pkey') - ) - op.create_table('account_deletion_logs', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('reason', sa.Text(), nullable=True), - sa.Column('account_id', models.types.StringUUID(), nullable=False), - sa.Column('snapshot', sa.Text(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), - sa.PrimaryKeyConstraint('id', name='account_deletion_log_pkey') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('account_deletion_logs') - op.drop_table('account_deletion_log_details') - # ### end Alembic commands ### From 8275a0fa2ae8133cf66a650eed3623e30cc30e78 Mon Sep 17 00:00:00 2001 From: GareArc Date: Mon, 23 Dec 2024 23:08:29 -0500 Subject: [PATCH 23/55] reformat --- api/configs/feature/__init__.py | 16 ++++++-- api/services/account_service.py | 63 ++++++++++++++++++-------------- api/tasks/delete_account_task.py | 3 +- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 3b6e2062aa6d71..e11993ddc7a1fe 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1,11 +1,19 @@ from typing import Annotated, Literal, Optional -from configs.feature.hosted_service import HostedServiceConfig -from pydantic import (AliasChoices, Field, HttpUrl, NegativeInt, - NonNegativeInt, PositiveFloat, PositiveInt, - computed_field) +from pydantic import ( + AliasChoices, + Field, + HttpUrl, + NegativeInt, + NonNegativeInt, + PositiveFloat, + PositiveInt, + computed_field, +) from pydantic_settings import BaseSettings +from configs.feature.hosted_service import HostedServiceConfig + class SecurityConfig(BaseSettings): """ diff --git a/api/services/account_service.py b/api/services/account_service.py index 48f68753d9f439..82bab0a07b2ea2 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,6 +8,11 @@ from hashlib import sha256 from typing import Any, Optional, cast +from api.services.billing_service import BillingService +from pydantic import BaseModel +from sqlalchemy import func +from werkzeug.exceptions import Unauthorized + from configs import dify_config from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created @@ -17,35 +22,40 @@ from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password from libs.rsa import generate_key_pair -from models.account import (Account, AccountIntegrate, AccountStatus, Tenant, - TenantAccountJoin, TenantAccountJoinRole, - TenantAccountRole, TenantStatus) +from models.account import ( + Account, + AccountIntegrate, + AccountStatus, + Tenant, + TenantAccountJoin, + TenantAccountJoinRole, + TenantAccountRole, + TenantStatus, +) from models.model import DifySetup -from pydantic import BaseModel -from services.errors.account import (AccountAlreadyInTenantError, - AccountLoginError, AccountNotFoundError, - AccountNotLinkTenantError, - AccountPasswordError, - AccountRegisterError, - CannotOperateSelfError, - CurrentPasswordIncorrectError, - InvalidActionError, - LinkAccountIntegrateError, - MemberNotInTenantError, NoPermissionError, - RoleAlreadyAssignedError, - TenantNotFoundError) +from services.errors.account import ( + AccountAlreadyInTenantError, + AccountLoginError, + AccountNotFoundError, + AccountNotLinkTenantError, + AccountPasswordError, + AccountRegisterError, + CannotOperateSelfError, + CurrentPasswordIncorrectError, + InvalidActionError, + LinkAccountIntegrateError, + MemberNotInTenantError, + NoPermissionError, + RoleAlreadyAssignedError, + TenantNotFoundError, +) from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService -from sqlalchemy import func 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_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_reset_password_task import send_reset_password_mail_task -from werkzeug.exceptions import Unauthorized - -from api.services.billing_service import BillingService class TokenPair(BaseModel): @@ -251,8 +261,7 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, @classmethod def send_account_deletion_verification_email(cls, account: Account, code: str): if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import \ - EmailCodeAccountDeletionRateLimitExceededError + from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError raise EmailCodeAccountDeletionRateLimitExceededError() @@ -388,8 +397,7 @@ def send_reset_password_email( raise ValueError("Email must be provided.") if cls.reset_password_rate_limiter.is_rate_limited(account_email): - from controllers.console.auth.error import \ - PasswordResetRateLimitExceededError + from controllers.console.auth.error import PasswordResetRateLimitExceededError raise PasswordResetRateLimitExceededError() @@ -425,8 +433,7 @@ def send_email_code_login_email( if email is None: raise ValueError("Email must be provided.") if cls.email_code_login_rate_limiter.is_rate_limited(email): - from controllers.console.auth.error import \ - EmailCodeLoginRateLimitExceededError + from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError raise EmailCodeLoginRateLimitExceededError() diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 465567c4ff175c..575f0bc4714472 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,6 +1,7 @@ import logging from celery import shared_task + from extensions.ext_database import db from models.account import Account from services.billing_service import BillingService @@ -14,7 +15,7 @@ def delete_account_task(account_id, reason: str): try: BillingService.delete_account(account_id, reason) except Exception as e: - logger.error(f"Failed to delete account {account_id} from billing service: {e}") + logger.exception(f"Failed to delete account {account_id} from billing service.") raise account = db.session.query(Account).filter(Account.id == account_id).first() From 4cf190f1ecbbe0350047c858c897250df969053a Mon Sep 17 00:00:00 2001 From: GareArc Date: Tue, 24 Dec 2024 04:50:13 +0000 Subject: [PATCH 24/55] fix wrong import --- api/services/account_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 82bab0a07b2ea2..63788d0f4aaf67 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,7 +8,7 @@ from hashlib import sha256 from typing import Any, Optional, cast -from api.services.billing_service import BillingService +from services.billing_service import BillingService from pydantic import BaseModel from sqlalchemy import func from werkzeug.exceptions import Unauthorized From 8b453f87ac0358dcacedab4e760e94530c346315 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 00:05:34 -0500 Subject: [PATCH 25/55] fix: change celery queue name --- api/services/account_service.py | 10 +++++++--- api/tasks/delete_account_task.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 63788d0f4aaf67..cf2b7d3d75a91c 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,7 +8,6 @@ from hashlib import sha256 from typing import Any, Optional, cast -from services.billing_service import BillingService from pydantic import BaseModel from sqlalchemy import func from werkzeug.exceptions import Unauthorized @@ -33,6 +32,7 @@ TenantStatus, ) from models.model import DifySetup +from services.billing_service import BillingService from services.errors.account import ( AccountAlreadyInTenantError, AccountLoginError, @@ -209,7 +209,9 @@ def create_account( raise AccountNotFound() if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountRegisterError("Email is in freeze.") + raise AccountRegisterError( + "Unable to re-register the account because the deletion occurred less than 30 days ago" + ) account = Account() account.email = email @@ -853,7 +855,9 @@ def register( db.session.begin_nested() """Register account""" if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountRegisterError("Email is in freeze.") + raise AccountRegisterError( + "Unable to re-register the account because the deletion occurred less than 30 days ago" + ) try: account = AccountService.create_account( email=email, diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 575f0bc4714472..86e382a32c8b17 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -@shared_task(queue="dataset") +@shared_task(queue="account_deletion") def delete_account_task(account_id, reason: str): try: BillingService.delete_account(account_id, reason) From 3492f15e4d0cbdd6cabf94b42bef266ab50168f8 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 00:14:29 -0500 Subject: [PATCH 26/55] fix: type check --- api/tasks/delete_account_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 86e382a32c8b17..961bc43f94c7f0 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -19,5 +19,8 @@ def delete_account_task(account_id, reason: str): raise account = db.session.query(Account).filter(Account.id == account_id).first() + if not account: + logger.error(f"Account {account_id} not found.") + return # send success email send_deletion_success_task.delay(account.interface_language, account.email) From 65eed9aac2c9087fed1508ee0253d514e194ac57 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 00:26:43 -0500 Subject: [PATCH 27/55] fix: bugs --- api/services/account_service.py | 2 +- api/services/billing_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index cf2b7d3d75a91c..4285097d106e20 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -262,12 +262,12 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, @classmethod def send_account_deletion_verification_email(cls, account: Account, code: str): + language, email = account.interface_language, account.email if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email): from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError raise EmailCodeAccountDeletionRateLimitExceededError() - language, email = account.interface_language, account.email send_account_deletion_verification_code.delay(language=language, to=email, code=code) cls.email_code_account_deletion_rate_limiter.increment_rate_limit(email) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 587cd704eba304..ad71607d677eb5 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -82,6 +82,6 @@ def is_email_in_freeze(cls, email: str) -> bool: params = {"email": email} try: response = cls._send_request("GET", "/account/in-freeze", params=params) - return response["data"] + return bool(response.get("data", False)) except Exception: return False From d313a71027b1ac7644ccceff7f0966633e313e4e Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 00:48:38 -0500 Subject: [PATCH 28/55] fix: ignore type for celey in mypy --- api/tasks/delete_account_task.py | 3 +-- api/tasks/mail_account_deletion_task.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 961bc43f94c7f0..65b3f7758f8180 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,7 +1,6 @@ import logging -from celery import shared_task - +from celery import shared_task # type: ignore from extensions.ext_database import db from models.account import Account from services.billing_service import BillingService diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py index d38df3613bd450..820924fc89a4c6 100644 --- a/api/tasks/mail_account_deletion_task.py +++ b/api/tasks/mail_account_deletion_task.py @@ -2,10 +2,9 @@ import time import click -from celery import shared_task -from flask import render_template - +from celery import shared_task # type: ignore from extensions.ext_mail import mail +from flask import render_template @shared_task(queue="mail") From f65fa9b8c6cb36df67114b0abc6d1cdad103bab6 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 00:51:36 -0500 Subject: [PATCH 29/55] reformat --- api/tasks/delete_account_task.py | 1 + api/tasks/mail_account_deletion_task.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 65b3f7758f8180..3f61425c62f036 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,6 +1,7 @@ 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 diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py index 820924fc89a4c6..8d1434dc55098e 100644 --- a/api/tasks/mail_account_deletion_task.py +++ b/api/tasks/mail_account_deletion_task.py @@ -3,9 +3,10 @@ import click from celery import shared_task # type: ignore -from extensions.ext_mail import mail from flask import render_template +from extensions.ext_mail import mail + @shared_task(queue="mail") def send_deletion_success_task(language, to): From 7c2d43f43948f61b2c60da7fe29a1c627ebc63d1 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 02:28:21 -0500 Subject: [PATCH 30/55] feat: add seperate feedback api --- api/controllers/console/workspace/account.py | 18 ++++++++++++++++++ api/services/billing_service.py | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index f10041546be47d..a974642a770536 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -24,6 +24,8 @@ from services.errors.account import \ CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError +from api.services.billing_service import BillingService + class AccountInitApi(Resource): @setup_required @@ -280,6 +282,21 @@ def post(self): return {"result": "success"} +class AccountDeleleUpdateFeedbackApi(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_eletion_feedback(args["email"], args["feedback"]) + + return {"result": "success"} + + # Register API resources api.add_resource(AccountInitApi, "/account/init") api.add_resource(AccountProfileApi, "/account/profile") @@ -292,5 +309,6 @@ def post(self): api.add_resource(AccountIntegrateApi, "/account/integrates") api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") api.add_resource(AccountDeleteApi, "/account/delete") +api.add_resource(AccountDeleleUpdateFeedbackApi, "/account/delete/feedback") # api.add_resource(AccountEmailApi, '/account/email') # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') diff --git a/api/services/billing_service.py b/api/services/billing_service.py index ad71607d677eb5..9e4347d2927f38 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -85,3 +85,9 @@ def is_email_in_freeze(cls, email: str) -> bool: return bool(response.get("data", False)) except Exception: return False + + @classmethod + def update_account_eletion_feedback(cls, email: str, feedback: str): + """Update account deletion feedback.""" + json = {"email": email, "feedback": feedback} + return cls._send_request("POST", "/account/deletion-feedback", json=json) From a1205ea739760b7cb96f2cdffae21c06d35d714f Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 02:48:25 -0500 Subject: [PATCH 31/55] fix: change task queue --- api/controllers/console/workspace/account.py | 5 ++--- api/services/billing_service.py | 2 +- api/tasks/delete_account_task.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index a974642a770536..81fe51cfccf2df 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -21,11 +21,10 @@ from libs.login import login_required from models import AccountIntegrate, InvitationCode from services.account_service import AccountService +from services.billing_service import BillingService from services.errors.account import \ CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError -from api.services.billing_service import BillingService - class AccountInitApi(Resource): @setup_required @@ -292,7 +291,7 @@ def post(self): parser.add_argument("feedback", type=str, required=True, location="json") args = parser.parse_args() - BillingService.update_account_eletion_feedback(args["email"], args["feedback"]) + BillingService.update_account_deletion_feedback(args["email"], args["feedback"]) return {"result": "success"} diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 9e4347d2927f38..d8bef8789acbdf 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -87,7 +87,7 @@ def is_email_in_freeze(cls, email: str) -> bool: return False @classmethod - def update_account_eletion_feedback(cls, email: str, feedback: str): + 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/deletion-feedback", json=json) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 3f61425c62f036..fa7a0ce0bff6ab 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -@shared_task(queue="account_deletion") +@shared_task(queue="dataset") def delete_account_task(account_id, reason: str): try: BillingService.delete_account(account_id, reason) From 018e932ed1eeb36a01ea91f81a0af9b7adc6e22d Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 03:31:52 -0500 Subject: [PATCH 32/55] fix: wrong api route --- api/controllers/console/workspace/account.py | 2 +- api/services/billing_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 81fe51cfccf2df..1e8748a5ce7479 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -274,7 +274,7 @@ def post(self): args = parser.parse_args() if not AccountService.verify_account_deletion_code(args["token"], args["code"]): - return {"result": "fail", "error": "Verification code is invalid."}, 400 + raise ValueError("Invalid verification code.") AccountService.delete_account(account, args["reason"]) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index d8bef8789acbdf..77d055a117a628 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -75,7 +75,7 @@ def is_tenant_owner_or_admin(current_user): def delete_account(cls, account_id: str, reason: str): """Delete account.""" params = {"account_id": account_id, "reason": reason} - return cls._send_request("DELETE", "/account", params=params) + return cls._send_request("DELETE", "/account/", params=params) @classmethod def is_email_in_freeze(cls, email: str) -> bool: From e96a6e666ecbe2e24caa786b9efbea97476852c2 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 04:08:02 -0500 Subject: [PATCH 33/55] fix: bug --- api/tasks/delete_account_task.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index fa7a0ce0bff6ab..921f9300253f4a 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,7 +1,6 @@ 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 @@ -12,13 +11,13 @@ @shared_task(queue="dataset") def delete_account_task(account_id, reason: str): + account = db.session.query(Account).filter(Account.id == account_id).first() try: BillingService.delete_account(account_id, reason) except Exception as e: logger.exception(f"Failed to delete account {account_id} from billing service.") raise - account = db.session.query(Account).filter(Account.id == account_id).first() if not account: logger.error(f"Account {account_id} not found.") return From a37ac27c690a380f805347455a459e4858431361 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 04:08:22 -0500 Subject: [PATCH 34/55] reformat --- api/tasks/delete_account_task.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 921f9300253f4a..d005e1178f9eb7 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -1,6 +1,7 @@ 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 e768fb2218647ffcb04d95b8da619c1cee48d2aa Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 14:32:08 -0500 Subject: [PATCH 35/55] fix: update api --- .../console/auth/forgot_password.py | 12 +++---- api/controllers/console/auth/login.py | 34 +++++++++---------- api/controllers/console/auth/oauth.py | 4 ++- api/controllers/console/error.py | 9 +++++ api/controllers/console/workspace/account.py | 20 +++++------ api/controllers/console/workspace/error.py | 6 ++++ api/services/account_service.py | 8 ++--- api/services/billing_service.py | 4 +-- api/tasks/delete_account_task.py | 4 +-- 9 files changed, 54 insertions(+), 47 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 140b9e145fa9cd..5423e9d2db062f 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -1,18 +1,14 @@ import base64 import secrets +from api.services.errors.account import AccountRegisterError from flask import request from flask_restful import Resource, reqparse # type: ignore from constants.languages import languages 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 AccountNotFound, AccountOnRegisterError, EmailSendIpLimitError from controllers.console.wraps import setup_required from events.tenant_event import tenant_was_created from extensions.ext_database import db @@ -129,6 +125,8 @@ def post(self): ) except WorkSpaceNotAllowedCreateError: pass + except AccountRegisterError as e: + return AccountOnRegisterError(message=str(e)) return {"result": "success"} diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 78a80fc8d7e075..5c1cfd2f078dfe 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,34 +1,32 @@ from typing import cast import flask_login # type: ignore -from flask import request -from flask_restful import Resource, reqparse # type: ignore - import services from constants.languages import languages from controllers.console import api -from controllers.console.auth.error import ( - EmailCodeError, - EmailOrPasswordMismatchError, - EmailPasswordLoginLimitError, - InvalidEmailError, - InvalidTokenError, -) -from controllers.console.error import ( - AccountBannedError, - AccountNotFound, - EmailSendIpLimitError, - NotAllowedCreateWorkspace, -) +from controllers.console.auth.error import (EmailCodeError, + EmailOrPasswordMismatchError, + EmailPasswordLoginLimitError, + InvalidEmailError, + InvalidTokenError) +from controllers.console.error import (AccountBannedError, AccountNotFound, + AccountOnRegisterError, + EmailSendIpLimitError, + NotAllowedCreateWorkspace) from controllers.console.wraps import setup_required from events.tenant_event import tenant_was_created +from flask import request +from flask_restful import Resource, reqparse # type: ignore 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.account_service import (AccountService, RegisterService, + TenantService) from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService +from api.services.errors.account import AccountRegisterError + class LoginApi(Resource): """Resource for user login.""" @@ -196,6 +194,8 @@ def post(self): ) except WorkSpaceNotAllowedCreateError: return NotAllowedCreateWorkspace() + except AccountRegisterError as e: + return AccountOnRegisterError(message=str(e)) token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token_pair.model_dump()} diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 333b24142727f0..8fdcb7e8b3e161 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -16,7 +16,7 @@ from models import Account from models.account import AccountStatus 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.feature_service import FeatureService @@ -99,6 +99,8 @@ def get(self, provider: str): f"{dify_config.CONSOLE_WEB_URL}/signin" "?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.message}") # Check account status if account.status == AccountStatus.BANNED.value: diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 1b4e6deae6b5b0..a2d92d9cbd8658 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -92,3 +92,12 @@ class UnauthorizedAndForceLogout(BaseHTTPException): error_code = "unauthorized_and_force_logout" description = "Unauthorized and force logout." code = 401 + + +class AccountOnRegisterError(BaseHTTPException): + error_code = "account_register_error" + code = 400 + + def __init__(self, message: str = ""): + description = f"Account register error: {message}." + super().__init__(description=description) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 1e8748a5ce7479..7b3c4014c9dc4c 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -4,10 +4,10 @@ from configs import dify_config from constants.languages import supported_language from controllers.console import api -from controllers.console.workspace.error import (AccountAlreadyInitedError, - CurrentPasswordIncorrectError, - InvalidInvitationCodeError, - RepeatPasswordNotMatchError) +from controllers.console.workspace.error import ( + AccountAlreadyInitedError, CurrentPasswordIncorrectError, + InvalidAccountDeletionCodeError, InvalidInvitationCodeError, + RepeatPasswordNotMatchError) from controllers.console.wraps import (account_initialization_required, enterprise_license_required, setup_required) @@ -251,11 +251,8 @@ class AccountDeleteVerifyApi(Resource): def get(self): account = current_user - try: - token, code = AccountService.generate_account_deletion_verification_code(account) - AccountService.send_account_delete_verification_email(account, code) - except Exception as e: - return {"result": "fail", "error": str(e)}, 429 + token, code = AccountService.generate_account_deletion_verification_code(account) + AccountService.send_account_delete_verification_email(account, code) return {"result": "success", "data": token} @@ -270,13 +267,12 @@ def post(self): parser = reqparse.RequestParser() parser.add_argument("token", type=str, required=True, location="json") parser.add_argument("code", type=str, required=True, location="json") - parser.add_argument("reason", type=str, required=True, location="json") args = parser.parse_args() if not AccountService.verify_account_deletion_code(args["token"], args["code"]): - raise ValueError("Invalid verification code.") + raise InvalidAccountDeletionCodeError() - AccountService.delete_account(account, args["reason"]) + AccountService.delete_account(account) return {"result": "success"} diff --git a/api/controllers/console/workspace/error.py b/api/controllers/console/workspace/error.py index 9e13c7b9241ff1..8b70ca62b92b70 100644 --- a/api/controllers/console/workspace/error.py +++ b/api/controllers/console/workspace/error.py @@ -35,3 +35,9 @@ class AccountNotInitializedError(BaseHTTPException): error_code = "account_not_initialized" description = "The account has not been initialized yet. Please proceed with the initialization process first." code = 400 + + +class InvalidAccountDeletionCodeError(BaseHTTPException): + error_code = "invalid_account_deletion_code" + description = "Invalid account deletion code." + code = 400 diff --git a/api/services/account_service.py b/api/services/account_service.py index 4285097d106e20..d83d5e429ea64b 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -284,9 +284,9 @@ def verify_account_deletion_code(token: str, code: str) -> bool: return True @staticmethod - def delete_account(account: Account, reason="") -> None: + 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, reason) + delete_account_task.delay(account.id) @staticmethod def link_account_integrate(provider: str, open_id: str, account: Account) -> None: @@ -854,10 +854,6 @@ def register( ) -> Account: db.session.begin_nested() """Register account""" - if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountRegisterError( - "Unable to re-register the account because the deletion occurred less than 30 days ago" - ) try: account = AccountService.create_account( email=email, diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 77d055a117a628..d2034643503d2b 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -72,9 +72,9 @@ def is_tenant_owner_or_admin(current_user): raise ValueError("Only team owner or team admin can perform this action") @classmethod - def delete_account(cls, account_id: str, reason: str): + def delete_account(cls, account_id: str): """Delete account.""" - params = {"account_id": account_id, "reason": reason} + params = {"account_id": account_id} return cls._send_request("DELETE", "/account/", params=params) @classmethod diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index d005e1178f9eb7..884918dc8b43a5 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -11,10 +11,10 @@ @shared_task(queue="dataset") -def delete_account_task(account_id, reason: str): +def delete_account_task(account_id): account = db.session.query(Account).filter(Account.id == account_id).first() try: - BillingService.delete_account(account_id, reason) + BillingService.delete_account(account_id) except Exception as e: logger.exception(f"Failed to delete account {account_id} from billing service.") raise From a9c0bc6203ba3b74539a31143bfd4f879c780b44 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 23:13:34 -0500 Subject: [PATCH 36/55] fix: add logic for blocking login when email is in freeze --- api/controllers/console/auth/forgot_password.py | 5 +---- api/controllers/console/auth/login.py | 5 ----- api/controllers/console/auth/oauth.py | 7 ++++--- api/controllers/console/error.py | 5 +---- api/controllers/console/workspace/account.py | 4 ++-- api/services/account_service.py | 10 ++++++++-- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 5423e9d2db062f..1f5989ddb18a94 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -1,14 +1,13 @@ import base64 import secrets -from api.services.errors.account import AccountRegisterError from flask import request from flask_restful import Resource, reqparse # type: ignore from constants.languages import languages from controllers.console import api from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError -from controllers.console.error import AccountNotFound, AccountOnRegisterError, EmailSendIpLimitError +from controllers.console.error import AccountNotFound, EmailSendIpLimitError from controllers.console.wraps import setup_required from events.tenant_event import tenant_was_created from extensions.ext_database import db @@ -125,8 +124,6 @@ def post(self): ) except WorkSpaceNotAllowedCreateError: pass - except AccountRegisterError as e: - return AccountOnRegisterError(message=str(e)) return {"result": "success"} diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 5c1cfd2f078dfe..8f835f483aa041 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -10,7 +10,6 @@ InvalidEmailError, InvalidTokenError) from controllers.console.error import (AccountBannedError, AccountNotFound, - AccountOnRegisterError, EmailSendIpLimitError, NotAllowedCreateWorkspace) from controllers.console.wraps import setup_required @@ -25,8 +24,6 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService -from api.services.errors.account import AccountRegisterError - class LoginApi(Resource): """Resource for user login.""" @@ -194,8 +191,6 @@ def post(self): ) except WorkSpaceNotAllowedCreateError: return NotAllowedCreateWorkspace() - except AccountRegisterError as e: - return AccountOnRegisterError(message=str(e)) token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token_pair.model_dump()} diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 8fdcb7e8b3e161..b0b655a5bb8502 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -3,6 +3,7 @@ from typing import Optional import requests +from api.controllers.console.error import AccountOnRegisterError from flask import current_app, redirect, request from flask_restful import Resource # type: ignore from werkzeug.exceptions import Unauthorized @@ -16,7 +17,7 @@ from models import Account from models.account import AccountStatus from services.account_service import AccountService, RegisterService, TenantService -from services.errors.account import AccountNotFoundError, AccountRegisterError +from services.errors.account import AccountNotFoundError from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError from services.feature_service import FeatureService @@ -99,8 +100,8 @@ def get(self, provider: str): f"{dify_config.CONSOLE_WEB_URL}/signin" "?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.message}") + except AccountOnRegisterError as e: + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}") # Check account status if account.status == AccountStatus.BANNED.value: diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index a2d92d9cbd8658..48c6ef488ba88d 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -97,7 +97,4 @@ class UnauthorizedAndForceLogout(BaseHTTPException): class AccountOnRegisterError(BaseHTTPException): error_code = "account_register_error" code = 400 - - def __init__(self, message: str = ""): - description = f"Account register error: {message}." - super().__init__(description=description) + description = "Account register error." diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 7b3c4014c9dc4c..ef5ba005e5693e 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -277,7 +277,7 @@ def post(self): return {"result": "success"} -class AccountDeleleUpdateFeedbackApi(Resource): +class AccountDeleteUpdateFeedbackApi(Resource): @setup_required def post(self): account = current_user @@ -304,6 +304,6 @@ def post(self): api.add_resource(AccountIntegrateApi, "/account/integrates") api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") api.add_resource(AccountDeleteApi, "/account/delete") -api.add_resource(AccountDeleleUpdateFeedbackApi, "/account/delete/feedback") +api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback") # api.add_resource(AccountEmailApi, '/account/email') # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') diff --git a/api/services/account_service.py b/api/services/account_service.py index d83d5e429ea64b..27224ac4f072b3 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,6 +8,7 @@ from hashlib import sha256 from typing import Any, Optional, cast +from api.controllers.console.error import AccountOnRegisterError from pydantic import BaseModel from sqlalchemy import func from werkzeug.exceptions import Unauthorized @@ -209,8 +210,8 @@ def create_account( raise AccountNotFound() if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountRegisterError( - "Unable to re-register the account because the deletion occurred less than 30 days ago" + raise AccountOnRegisterError( + description="Unable to re-register the account because the deletion occurred less than 30 days ago" ) account = Account() @@ -468,6 +469,11 @@ def get_user_through_email(cls, email: str): if account.status == AccountStatus.BANNED.value: raise Unauthorized("Account is banned.") + if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): + raise AccountOnRegisterError( + description="Unable to re-register the account because the deletion occurred less than 30 days ago" + ) + return account @staticmethod From 99a0178c17a7793c6147e79489eca6b88c7dd552 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 23:18:06 -0500 Subject: [PATCH 37/55] reformat --- api/controllers/console/auth/oauth.py | 2 +- api/services/account_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index b0b655a5bb8502..cde271a14fc638 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -3,13 +3,13 @@ from typing import Optional import requests -from api.controllers.console.error import AccountOnRegisterError from flask import current_app, redirect, request from flask_restful import Resource # type: ignore from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import languages +from controllers.console.error import AccountOnRegisterError from events.tenant_event import tenant_was_created from extensions.ext_database import db from libs.helper import extract_remote_ip diff --git a/api/services/account_service.py b/api/services/account_service.py index 27224ac4f072b3..0a93afc5cccaa4 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -8,13 +8,13 @@ from hashlib import sha256 from typing import Any, Optional, cast -from api.controllers.console.error import AccountOnRegisterError from pydantic import BaseModel from sqlalchemy import func from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import language_timezone_mapping, languages +from controllers.console.error import AccountOnRegisterError from events.tenant_event import tenant_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client From e6bf5e47251158da8c7a81dd83bb79114c4fc964 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 25 Dec 2024 23:26:39 -0500 Subject: [PATCH 38/55] reformat --- api/controllers/console/auth/login.py | 29 ++++++++++++-------- api/controllers/console/workspace/account.py | 24 ++++++++-------- api/libs/helper.py | 5 ++-- api/services/billing_service.py | 4 +-- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 8f835f483aa041..78a80fc8d7e075 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,26 +1,31 @@ from typing import cast import flask_login # type: ignore +from flask import request +from flask_restful import Resource, reqparse # type: ignore + import services from constants.languages import languages from controllers.console import api -from controllers.console.auth.error import (EmailCodeError, - EmailOrPasswordMismatchError, - EmailPasswordLoginLimitError, - InvalidEmailError, - InvalidTokenError) -from controllers.console.error import (AccountBannedError, AccountNotFound, - EmailSendIpLimitError, - NotAllowedCreateWorkspace) +from controllers.console.auth.error import ( + EmailCodeError, + EmailOrPasswordMismatchError, + EmailPasswordLoginLimitError, + InvalidEmailError, + InvalidTokenError, +) +from controllers.console.error import ( + AccountBannedError, + AccountNotFound, + EmailSendIpLimitError, + NotAllowedCreateWorkspace, +) from controllers.console.wraps import setup_required from events.tenant_event import tenant_was_created -from flask import request -from flask_restful import Resource, reqparse # type: ignore 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.account_service import AccountService, RegisterService, TenantService from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index ef5ba005e5693e..a1b7a554674cac 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -1,29 +1,29 @@ import datetime import pytz +from flask import request +from flask_login import current_user # type: ignore +from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore + from configs import dify_config from constants.languages import supported_language from controllers.console import api from controllers.console.workspace.error import ( - AccountAlreadyInitedError, CurrentPasswordIncorrectError, - InvalidAccountDeletionCodeError, InvalidInvitationCodeError, - RepeatPasswordNotMatchError) -from controllers.console.wraps import (account_initialization_required, - enterprise_license_required, - setup_required) + AccountAlreadyInitedError, + CurrentPasswordIncorrectError, + InvalidAccountDeletionCodeError, + InvalidInvitationCodeError, + RepeatPasswordNotMatchError, +) +from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required from extensions.ext_database import db from fields.member_fields import account_fields -from flask import request -from flask_login import current_user # type: ignore -from flask_restful import (Resource, fields, marshal_with, # type: ignore - reqparse) from libs.helper import TimestampField, timezone from libs.login import login_required from models import AccountIntegrate, InvitationCode 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 class AccountInitApi(Resource): diff --git a/api/libs/helper.py b/api/libs/helper.py index e94ff348d49e27..eaa4efdb714355 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -12,12 +12,13 @@ from typing import Any, Optional, Union, cast from zoneinfo import available_timezones +from flask import Response, stream_with_context +from flask_restful import fields # type: ignore + from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.file import helpers as file_helpers from extensions.ext_redis import redis_client -from flask import Response, stream_with_context -from flask_restful import fields # type: ignore from models.account import Account diff --git a/api/services/billing_service.py b/api/services/billing_service.py index d2034643503d2b..0fc619f54287a8 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -2,10 +2,10 @@ from typing import Optional import httpx +from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fixed + from extensions.ext_database import db from models.account import TenantAccountJoin, TenantAccountRole -from tenacity import (retry, retry_if_exception_type, stop_before_delay, - wait_fixed) class BillingService: From aef4e984ebf1d2d733268fce3be53c6fe8e2b24d Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 00:04:08 -0500 Subject: [PATCH 39/55] fix: wrong error object --- api/controllers/console/auth/forgot_password.py | 5 ++++- api/controllers/console/auth/login.py | 4 ++++ api/controllers/console/auth/oauth.py | 7 +++---- api/services/account_service.py | 5 ++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 1f5989ddb18a94..0b1cc712a0e8ca 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -1,13 +1,14 @@ import base64 import secrets +from api.services.errors.account import AccountRegisterError from flask import request from flask_restful import Resource, reqparse # type: ignore from constants.languages import languages 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.error import AccountNotFound, AccountOnRegisterError, EmailSendIpLimitError from controllers.console.wraps import setup_required from events.tenant_event import tenant_was_created from extensions.ext_database import db @@ -124,6 +125,8 @@ def post(self): ) except WorkSpaceNotAllowedCreateError: pass + except AccountRegisterError as are: + raise AccountOnRegisterError(description=str(are)) return {"result": "success"} diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 78a80fc8d7e075..730af1b019649b 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,6 +1,7 @@ from typing import cast import flask_login # type: ignore +from api.services.errors.account import AccountRegisterError from flask import request from flask_restful import Resource, reqparse # type: ignore @@ -17,6 +18,7 @@ from controllers.console.error import ( AccountBannedError, AccountNotFound, + AccountOnRegisterError, EmailSendIpLimitError, NotAllowedCreateWorkspace, ) @@ -196,6 +198,8 @@ def post(self): ) except WorkSpaceNotAllowedCreateError: return NotAllowedCreateWorkspace() + except AccountRegisterError as are: + raise AccountOnRegisterError(description=str(are)) token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token_pair.model_dump()} diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index cde271a14fc638..829af2b94bb01b 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -9,7 +9,6 @@ from configs import dify_config from constants.languages import languages -from controllers.console.error import AccountOnRegisterError from events.tenant_event import tenant_was_created from extensions.ext_database import db from libs.helper import extract_remote_ip @@ -17,7 +16,7 @@ from models import Account from models.account import AccountStatus 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.feature_service import FeatureService @@ -100,8 +99,8 @@ def get(self, provider: str): f"{dify_config.CONSOLE_WEB_URL}/signin" "?message=Workspace not found, please contact system admin to invite you to join in a workspace." ) - except AccountOnRegisterError as e: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}") + except AccountRegisterError as e: + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={str(e)}") # Check account status if account.status == AccountStatus.BANNED.value: diff --git a/api/services/account_service.py b/api/services/account_service.py index 0a93afc5cccaa4..602a48bd14c0b5 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -14,7 +14,6 @@ from configs import dify_config from constants.languages import language_timezone_mapping, languages -from controllers.console.error import AccountOnRegisterError from events.tenant_event import tenant_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client @@ -210,7 +209,7 @@ def create_account( raise AccountNotFound() if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountOnRegisterError( + raise AccountRegisterError( description="Unable to re-register the account because the deletion occurred less than 30 days ago" ) @@ -470,7 +469,7 @@ def get_user_through_email(cls, email: str): raise Unauthorized("Account is banned.") if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountOnRegisterError( + raise AccountRegisterError( description="Unable to re-register the account because the deletion occurred less than 30 days ago" ) From 6f02bad351b72f12e5551fc4c09d9b205ed8c00a Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 00:09:18 -0500 Subject: [PATCH 40/55] reformat --- api/controllers/console/auth/forgot_password.py | 2 +- api/controllers/console/auth/login.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 0b1cc712a0e8ca..ade2248d476dba 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -1,7 +1,6 @@ import base64 import secrets -from api.services.errors.account import AccountRegisterError from flask import request from flask_restful import Resource, reqparse # type: ignore @@ -16,6 +15,7 @@ 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 from services.feature_service import FeatureService diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 730af1b019649b..6f0affef8def19 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,7 +1,6 @@ from typing import cast import flask_login # type: ignore -from api.services.errors.account import AccountRegisterError from flask import request from flask_restful import Resource, reqparse # type: ignore @@ -28,6 +27,7 @@ from libs.password import valid_password from models.account import Account from services.account_service import AccountService, RegisterService, TenantService +from services.errors.account import AccountRegisterError from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService From 271dd9d76f4566fbcfcbbabb1894b3cf9b0e5408 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 00:26:01 -0500 Subject: [PATCH 41/55] fix: error handling --- api/controllers/console/auth/login.py | 16 ++++++++++++---- api/services/account_service.py | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 6f0affef8def19..9565a9132be708 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -115,8 +115,10 @@ def post(self): language = "zh-Hans" else: 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 AccountOnRegisterError(description=str(are)) if account is None: if FeatureService.get_system_features().is_allow_register: token = AccountService.send_reset_password_email(email=args["email"], language=language) @@ -144,8 +146,11 @@ def post(self): language = "zh-Hans" else: language = "en-US" + try: + account = AccountService.get_user_through_email(args["email"]) + except AccountRegisterError as are: + raise AccountOnRegisterError(description=str(are)) - account = AccountService.get_user_through_email(args["email"]) if account is None: if FeatureService.get_system_features().is_allow_register: token = AccountService.send_email_code_login_email(email=args["email"], language=language) @@ -179,7 +184,10 @@ def post(self): raise EmailCodeError() 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 AccountOnRegisterError(description=str(are)) if account: tenant = TenantService.get_join_tenants(account) if not tenant: diff --git a/api/services/account_service.py b/api/services/account_service.py index 602a48bd14c0b5..b7720bcf918848 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -432,6 +432,7 @@ def send_account_delete_verification_email(cls, account: Account, code: str): def send_email_code_login_email( 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: raise ValueError("Email must be provided.") if cls.email_code_login_rate_limiter.is_rate_limited(email): From 056c95f139f7ff9a3ef5747959fea2b5f863e70c Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 00:49:18 -0500 Subject: [PATCH 42/55] fix: error description --- api/controllers/console/auth/forgot_password.py | 2 +- api/controllers/console/auth/login.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index ade2248d476dba..9a6349b168ee24 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -126,7 +126,7 @@ def post(self): except WorkSpaceNotAllowedCreateError: pass except AccountRegisterError as are: - raise AccountOnRegisterError(description=str(are)) + raise AccountOnRegisterError(description=are.description) return {"result": "success"} diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 9565a9132be708..f623261c655148 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -118,7 +118,7 @@ def post(self): try: account = AccountService.get_user_through_email(args["email"]) except AccountRegisterError as are: - raise AccountOnRegisterError(description=str(are)) + raise AccountOnRegisterError(description=are.description) if account is None: if FeatureService.get_system_features().is_allow_register: token = AccountService.send_reset_password_email(email=args["email"], language=language) @@ -149,7 +149,7 @@ def post(self): try: account = AccountService.get_user_through_email(args["email"]) except AccountRegisterError as are: - raise AccountOnRegisterError(description=str(are)) + raise AccountOnRegisterError(description=are.description) if account is None: if FeatureService.get_system_features().is_allow_register: @@ -187,7 +187,7 @@ def post(self): try: account = AccountService.get_user_through_email(user_email) except AccountRegisterError as are: - raise AccountOnRegisterError(description=str(are)) + raise AccountOnRegisterError(description=are.description) if account: tenant = TenantService.get_join_tenants(account) if not tenant: @@ -207,7 +207,7 @@ def post(self): except WorkSpaceNotAllowedCreateError: return NotAllowedCreateWorkspace() except AccountRegisterError as are: - raise AccountOnRegisterError(description=str(are)) + raise AccountOnRegisterError(description=are.description) token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token_pair.model_dump()} From cc7954300009f174130f9cf99884642abfefd377 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 01:02:56 -0500 Subject: [PATCH 43/55] fix: reject before sending email --- api/services/account_service.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index b7720bcf918848..d95b5451057355 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -462,6 +462,11 @@ def revoke_email_code_login_token(cls, token: str): @classmethod def get_user_through_email(cls, email: str): + if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): + raise AccountRegisterError( + description="Unable to re-register the account because the deletion occurred less than 30 days ago" + ) + account = db.session.query(Account).filter(Account.email == email).first() if not account: return None @@ -469,11 +474,6 @@ def get_user_through_email(cls, email: str): if account.status == AccountStatus.BANNED.value: raise Unauthorized("Account is banned.") - if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): - raise AccountRegisterError( - description="Unable to re-register the account because the deletion occurred less than 30 days ago" - ) - return account @staticmethod From b5b989b7be4a83a84107d373878961dacd1999b6 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 01:47:31 -0500 Subject: [PATCH 44/55] fix: block login if email in freeze --- api/controllers/console/auth/login.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index f623261c655148..262734e9b2925e 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -5,6 +5,7 @@ from flask_restful import Resource, reqparse # type: ignore import services +from configs import dify_config from constants.languages import languages from controllers.console import api from controllers.console.auth.error import ( @@ -27,6 +28,7 @@ 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 from services.errors.account import AccountRegisterError from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService @@ -46,6 +48,11 @@ def post(self): 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(email): + raise AccountOnRegisterError( + description="Unable to re-register the account because the deletion occurred less than 30 days ago" + ) + is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) if is_login_error_rate_limit: raise EmailPasswordLoginLimitError() From 3c2d401ce9d40b7b85ad64bdb56e71f8a6ddd943 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 01:54:58 -0500 Subject: [PATCH 45/55] fix: wrong email value --- api/controllers/console/auth/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 262734e9b2925e..dd1f659460adb7 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -48,7 +48,7 @@ def post(self): 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(email): + if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]): raise AccountOnRegisterError( description="Unable to re-register the account because the deletion occurred less than 30 days ago" ) From e1272830899fd2ada581bd12f049d8942e4dd9ca Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 26 Dec 2024 15:45:00 +0800 Subject: [PATCH 46/55] fix: update email template --- ...te_account_code_email_template_en-US.html} | 23 ++- ...ete_account_code_email_template_zh-CN.html | 74 -------- ...delete_account_success_template_en-US.html | 165 ++++++++++-------- ...delete_account_success_template_zh-CN.html | 77 -------- 4 files changed, 108 insertions(+), 231 deletions(-) rename api/templates/{delete_account_code_email_en-US.html => delete_account_code_email_template_en-US.html} (62%) delete mode 100644 api/templates/delete_account_code_email_template_zh-CN.html delete mode 100644 api/templates/delete_account_success_template_zh-CN.html diff --git a/api/templates/delete_account_code_email_en-US.html b/api/templates/delete_account_code_email_template_en-US.html similarity index 62% rename from api/templates/delete_account_code_email_en-US.html rename to api/templates/delete_account_code_email_template_en-US.html index b7f236be3c2da1..9270c1f2d11a58 100644 --- a/api/templates/delete_account_code_email_en-US.html +++ b/api/templates/delete_account_code_email_template_en-US.html @@ -12,7 +12,6 @@ } .container { width: 600px; - height: 360px; margin: 40px auto; padding: 36px 48px; background-color: #fcfcfd; @@ -63,12 +62,22 @@ Dify Logo -

Delete your Dify account

-

Copy and paste this code, this code will only be valid for the next 5 minutes.

-
- {{code}} -
-

If you didn't request, don't worry. You can safely ignore this email.

+

Dify.AI Account Deletion and Verification

+

+

Hi,

+

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:

+
+ {{code}} +
+

To complete the account deletion process:

+

1. Return to the account deletion page on our website

+

2. Enter the verification code above

+

3. Click "Confirm Deletion"

+

Please note:

+

- This code is valid for 5 minutes

+

- As the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion.

+

- All your user data will be queued for permanent deletion.

+

diff --git a/api/templates/delete_account_code_email_template_zh-CN.html b/api/templates/delete_account_code_email_template_zh-CN.html deleted file mode 100644 index 5a1649402cefcb..00000000000000 --- a/api/templates/delete_account_code_email_template_zh-CN.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - -
-
- - Dify Logo -
-

Dify 的删除账户验证码

-

复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。

-
- {{code}} -
-

如果您没有请求删除账户,请不要担心。您可以安全地忽略此电子邮件。

-
- - diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html index d10a913da685d2..b3f9d9d8f0d95b 100644 --- a/api/templates/delete_account_success_template_en-US.html +++ b/api/templates/delete_account_success_template_en-US.html @@ -1,76 +1,95 @@ - - - - -
-
- - Dify Logo -
-

Your account has finished deleting

-

You recently deleted your Dify account. It's done processing and is now deleted. As a reminder, you no longer have access to all the workspaces you used to be in with this account.

+ + + + + + +
+
+ + Dify Logo
- - +

Your Dify.AI Account Has Been Successfully Deleted

+

Hi,

+

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.

+

Thank you for being a part of the Dify.AI community.

+

Best regards,

+

Dify.AI Team

+
+ + + \ No newline at end of file diff --git a/api/templates/delete_account_success_template_zh-CN.html b/api/templates/delete_account_success_template_zh-CN.html deleted file mode 100644 index 7381dba81c19d1..00000000000000 --- a/api/templates/delete_account_success_template_zh-CN.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - -
-
- - Dify Logo -
-

您的账户已删除

-

您最近删除了您的 Dify 账户 。系统已完成处理,现在此账户已被删除。请注意,您不再有权访问此帐户曾经所在的所有空间。

-
- - From ae0aee5a7aac1732231bcd23bad1ab6b203c8590 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 02:55:28 -0500 Subject: [PATCH 47/55] fix: update email workflow --- api/controllers/console/workspace/account.py | 2 +- api/services/account_service.py | 9 ++---- api/tasks/delete_account_task.py | 2 +- api/tasks/mail_account_deletion_task.py | 32 ++++++-------------- 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index a1b7a554674cac..f1ec0f3d298db3 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -252,7 +252,7 @@ def get(self): account = current_user token, code = AccountService.generate_account_deletion_verification_code(account) - AccountService.send_account_delete_verification_email(account, code) + AccountService.send_account_deletion_verification_email(account, code) return {"result": "success", "data": token} diff --git a/api/services/account_service.py b/api/services/account_service.py index d95b5451057355..b891e8b1f1d4bc 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -262,13 +262,13 @@ def generate_account_deletion_verification_code(account: Account) -> tuple[str, @classmethod def send_account_deletion_verification_email(cls, account: Account, code: str): - language, email = account.interface_language, account.email + 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(language=language, to=email, code=code) + send_account_deletion_verification_code.delay(to=email, code=code) cls.email_code_account_deletion_rate_limiter.increment_rate_limit(email) @@ -423,11 +423,6 @@ def revoke_reset_password_token(cls, token: str): def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]: return TokenManager.get_token_data(token, "reset_password") - @classmethod - def send_account_delete_verification_email(cls, account: Account, code: str): - language, email = account.interface_language, account.email - send_account_deletion_verification_code.delay(language=language, to=email, code=code) - @classmethod def send_email_code_login_email( cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" diff --git a/api/tasks/delete_account_task.py b/api/tasks/delete_account_task.py index 884918dc8b43a5..52c884ca29e3dc 100644 --- a/api/tasks/delete_account_task.py +++ b/api/tasks/delete_account_task.py @@ -23,4 +23,4 @@ def delete_account_task(account_id): logger.error(f"Account {account_id} not found.") return # send success email - send_deletion_success_task.delay(account.interface_language, account.email) + send_deletion_success_task.delay(account.email) diff --git a/api/tasks/mail_account_deletion_task.py b/api/tasks/mail_account_deletion_task.py index 8d1434dc55098e..49a3a6d280c1c9 100644 --- a/api/tasks/mail_account_deletion_task.py +++ b/api/tasks/mail_account_deletion_task.py @@ -9,7 +9,7 @@ @shared_task(queue="mail") -def send_deletion_success_task(language, to): +def send_deletion_success_task(to): """Send email to user regarding account deletion. Args: @@ -22,20 +22,12 @@ def send_deletion_success_task(language, to): start_at = time.perf_counter() try: - if language == "zh-Hans": - html_content = render_template( - "delete_account_success_template_zh-CN.html", - to=to, - email=to, - ) - mail.send(to=to, subject="Dify 账户删除成功", html=html_content) - else: - html_content = render_template( - "delete_account_success_template_en-US.html", - to=to, - email=to, - ) - mail.send(to=to, subject="Dify Account Deleted", html=html_content) + 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( @@ -48,7 +40,7 @@ def send_deletion_success_task(language, to): @shared_task(queue="mail") -def send_account_deletion_verification_code(language, to, code): +def send_account_deletion_verification_code(to, code): """Send email to user regarding account deletion verification code. Args: @@ -62,12 +54,8 @@ def send_account_deletion_verification_code(language, to, code): start_at = time.perf_counter() try: - if language == "zh-Hans": - html_content = render_template("delete_account_code_email_template_zh-CN.html", to=to, code=code) - mail.send(to=to, subject="Dify 的删除账户验证码", html=html_content) - else: - html_content = render_template("delete_account_code_email_en-US.html", to=to, code=code) - mail.send(to=to, subject="Delete Your Dify Account", html=html_content) + 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( From f008c2660bd4f7e6d80459dad5a541f7d37bd91f Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 04:18:29 -0500 Subject: [PATCH 48/55] fix: remove redundant error description --- api/controllers/console/auth/forgot_password.py | 4 ++-- api/controllers/console/auth/login.py | 14 ++++++-------- api/controllers/console/error.py | 12 +++++++----- api/services/account_service.py | 10 ++++++++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 9a6349b168ee24..a9c4300b9a27c3 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -7,7 +7,7 @@ from constants.languages import languages from controllers.console import api from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError -from controllers.console.error import AccountNotFound, AccountOnRegisterError, EmailSendIpLimitError +from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError from controllers.console.wraps import setup_required from events.tenant_event import tenant_was_created from extensions.ext_database import db @@ -126,7 +126,7 @@ def post(self): except WorkSpaceNotAllowedCreateError: pass except AccountRegisterError as are: - raise AccountOnRegisterError(description=are.description) + raise AccountInFreezeError() return {"result": "success"} diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index dd1f659460adb7..41362e9fa22ff2 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -17,8 +17,8 @@ ) from controllers.console.error import ( AccountBannedError, + AccountInFreezeError, AccountNotFound, - AccountOnRegisterError, EmailSendIpLimitError, NotAllowedCreateWorkspace, ) @@ -49,9 +49,7 @@ def post(self): args = parser.parse_args() if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]): - raise AccountOnRegisterError( - description="Unable to re-register the account because the deletion occurred less than 30 days ago" - ) + raise AccountInFreezeError() is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) if is_login_error_rate_limit: @@ -125,7 +123,7 @@ def post(self): try: account = AccountService.get_user_through_email(args["email"]) except AccountRegisterError as are: - raise AccountOnRegisterError(description=are.description) + raise AccountInFreezeError() if account is None: if FeatureService.get_system_features().is_allow_register: token = AccountService.send_reset_password_email(email=args["email"], language=language) @@ -156,7 +154,7 @@ def post(self): try: account = AccountService.get_user_through_email(args["email"]) except AccountRegisterError as are: - raise AccountOnRegisterError(description=are.description) + raise AccountInFreezeError() if account is None: if FeatureService.get_system_features().is_allow_register: @@ -194,7 +192,7 @@ def post(self): try: account = AccountService.get_user_through_email(user_email) except AccountRegisterError as are: - raise AccountOnRegisterError(description=are.description) + raise AccountInFreezeError() if account: tenant = TenantService.get_join_tenants(account) if not tenant: @@ -214,7 +212,7 @@ def post(self): except WorkSpaceNotAllowedCreateError: return NotAllowedCreateWorkspace() except AccountRegisterError as are: - raise AccountOnRegisterError(description=are.description) + raise AccountInFreezeError() token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token_pair.model_dump()} diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 48c6ef488ba88d..04ad9e874b9686 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -93,8 +93,10 @@ class UnauthorizedAndForceLogout(BaseHTTPException): description = "Unauthorized and force logout." code = 401 - -class AccountOnRegisterError(BaseHTTPException): - error_code = "account_register_error" - code = 400 - description = "Account register error." + 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." + ) diff --git a/api/services/account_service.py b/api/services/account_service.py index b891e8b1f1d4bc..08af88e63f7df2 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -210,7 +210,10 @@ def create_account( if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): raise AccountRegisterError( - description="Unable to re-register the account because the deletion occurred less than 30 days ago" + description=( + "This email account has been deleted within the past " + "30 days and is temporarily unavailable for new account registration" + ) ) account = Account() @@ -459,7 +462,10 @@ def revoke_email_code_login_token(cls, token: str): def get_user_through_email(cls, email: str): if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): raise AccountRegisterError( - description="Unable to re-register the account because the deletion occurred less than 30 days ago" + 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() From 7ac41139b00911e8b50d47dd3dc53e160923ea56 Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 26 Dec 2024 17:29:31 +0800 Subject: [PATCH 49/55] fix: update delete account email template style --- ...ete_account_code_email_template_en-US.html | 201 +++++++++++------- ...delete_account_success_template_en-US.html | 18 +- 2 files changed, 134 insertions(+), 85 deletions(-) diff --git a/api/templates/delete_account_code_email_template_en-US.html b/api/templates/delete_account_code_email_template_en-US.html index 9270c1f2d11a58..7037fab344720a 100644 --- a/api/templates/delete_account_code_email_template_en-US.html +++ b/api/templates/delete_account_code_email_template_en-US.html @@ -1,83 +1,124 @@ - - - - -
-
- - Dify Logo -
-

Dify.AI Account Deletion and Verification

-

-

Hi,

-

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:

-
- {{code}} -
-

To complete the account deletion process:

-

1. Return to the account deletion page on our website

-

2. Enter the verification code above

-

3. Click "Confirm Deletion"

-

Please note:

-

- This code is valid for 5 minutes

-

- As the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion.

-

- All your user data will be queued for permanent deletion.

-

+ + + + + + +
+
+ + Dify Logo
- - +

Dify.AI Account Deletion and Verification

+

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:

+
+ {{code}} +
+
+

To complete the account deletion process:

+

1. Return to the account deletion page on our website

+

2. Enter the verification code above

+

3. Click "Confirm Deletion"

+
+

Please note:

+
    +
  • This code is valid for 5 minutes
  • +
  • As the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion.
  • +
  • All your user data will be queued for permanent deletion.
  • +
+
+ + + \ No newline at end of file diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html index b3f9d9d8f0d95b..3b4c8f745d2b7a 100644 --- a/api/templates/delete_account_success_template_en-US.html +++ b/api/templates/delete_account_success_template_en-US.html @@ -70,6 +70,15 @@ 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; + } @@ -80,15 +89,14 @@ Dify Logo

Your Dify.AI Account Has Been Successfully Deleted

-

Hi,

-

We're writing to confirm that your Dify.AI account has been successfully deleted as per your request. Your +

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.

-

Thank you for being a part of the Dify.AI community.

-

Best regards,

-

Dify.AI Team

+

Thank you for being a part of the Dify.AI community.

+

Best regards,

+

Dify.AI Team

From d45958a511333a91c65c6c54e169900af576a049 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 04:31:30 -0500 Subject: [PATCH 50/55] fix: bug --- api/controllers/console/error.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 04ad9e874b9686..ee87138a44602a 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -93,10 +93,11 @@ class UnauthorizedAndForceLogout(BaseHTTPException): description = "Unauthorized and force logout." 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." - ) + +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 79c96aad39ec85773224d5b6ead45aafc08e1fab Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 26 Dec 2024 17:40:30 +0800 Subject: [PATCH 51/55] fix: update delete account email template style --- api/templates/delete_account_success_template_en-US.html | 1 + 1 file changed, 1 insertion(+) diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html index 3b4c8f745d2b7a..17e03a28d37bce 100644 --- a/api/templates/delete_account_success_template_en-US.html +++ b/api/templates/delete_account_success_template_en-US.html @@ -35,6 +35,7 @@ font-weight: 600; font-size: 24px; line-height: 28.8px; + margin-bottom: 12px; } .description { From f0c30eabca3ba0507aa88a04768756c2d134b014 Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 26 Dec 2024 17:44:31 +0800 Subject: [PATCH 52/55] fix: update delete account email template style --- api/templates/delete_account_code_email_template_en-US.html | 1 + api/templates/delete_account_success_template_en-US.html | 1 + 2 files changed, 2 insertions(+) diff --git a/api/templates/delete_account_code_email_template_en-US.html b/api/templates/delete_account_code_email_template_en-US.html index 7037fab344720a..7707385334eca1 100644 --- a/api/templates/delete_account_code_email_template_en-US.html +++ b/api/templates/delete_account_code_email_template_en-US.html @@ -14,6 +14,7 @@ .container { width: 600px; + min-height: 605px; margin: 40px auto; padding: 36px 48px; background-color: #fcfcfd; diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html index 17e03a28d37bce..c5df75cabce093 100644 --- a/api/templates/delete_account_success_template_en-US.html +++ b/api/templates/delete_account_success_template_en-US.html @@ -14,6 +14,7 @@ .container { width: 600px; + min-height: 380px; margin: 40px auto; padding: 36px 48px; background-color: #fcfcfd; From 04cb7bf4b52bb28fc2e4a429692565e732f098c4 Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 26 Dec 2024 05:00:22 -0500 Subject: [PATCH 53/55] fix: add account register error message --- api/controllers/console/auth/oauth.py | 2 +- api/services/account_service.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 829af2b94bb01b..2a08362c6d62a9 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -100,7 +100,7 @@ def get(self, provider: str): "?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={str(e)}") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}") # Check account status if account.status == AccountStatus.BANNED.value: diff --git a/api/services/account_service.py b/api/services/account_service.py index 08af88e63f7df2..35bd0ed58e22ec 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -884,6 +884,10 @@ def register( db.session.commit() except WorkSpaceNotAllowedCreateError: db.session.rollback() + except AccountRegisterError as are: + db.session.rollback() + logging.exception("Register failed") + raise are except Exception as e: db.session.rollback() logging.exception("Register failed") From 0169c6d5d0a1c24f013d20c3ccb4bc8d172fb4bf Mon Sep 17 00:00:00 2001 From: GareArc Date: Fri, 27 Dec 2024 13:18:23 -0500 Subject: [PATCH 54/55] fix: use only numbers for verification code --- api/services/account_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 35bd0ed58e22ec..64477480dbea91 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -17,7 +17,7 @@ from events.tenant_event import tenant_was_created from extensions.ext_database import db from extensions.ext_redis import redis_client -from libs.helper import RateLimiter, TokenManager, generate_string +from libs.helper import RateLimiter, TokenManager from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password from libs.rsa import generate_key_pair @@ -257,7 +257,7 @@ def create_account_and_tenant( @staticmethod def generate_account_deletion_verification_code(account: Account) -> tuple[str, str]: - code = generate_string(6) + 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} ) From 30c17e5e7a2bdd066260234c203f78ffa8314035 Mon Sep 17 00:00:00 2001 From: GareArc Date: Fri, 27 Dec 2024 13:32:23 -0500 Subject: [PATCH 55/55] fix: wrong api url --- api/services/billing_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 0fc619f54287a8..3a13c10102fab8 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -90,4 +90,4 @@ def is_email_in_freeze(cls, email: str) -> bool: 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/deletion-feedback", json=json) + return cls._send_request("POST", "/account/delete-feedback", json=json)