From 42e6bf53c219f0f1468f92a13a45a22831a152ca Mon Sep 17 00:00:00 2001 From: douxc Date: Wed, 28 Aug 2024 11:37:54 +0800 Subject: [PATCH 01/97] feat: add email templates for login and reset password --- .../email_code_login_mail_template_en-US.html | 74 ++++++++++ .../email_code_login_mail_template_zh-CN.html | 74 ++++++++++ .../reset_password_mail_template_en-US.html | 132 +++++++++--------- .../reset_password_mail_template_zh-CN.html | 132 +++++++++--------- 4 files changed, 282 insertions(+), 130 deletions(-) create mode 100644 api/templates/email_code_login_mail_template_en-US.html create mode 100644 api/templates/email_code_login_mail_template_zh-CN.html diff --git a/api/templates/email_code_login_mail_template_en-US.html b/api/templates/email_code_login_mail_template_en-US.html new file mode 100644 index 0000000000..066818d10c --- /dev/null +++ b/api/templates/email_code_login_mail_template_en-US.html @@ -0,0 +1,74 @@ + + + + + + +
+
+ + Dify Logo +
+

Your login code for Dify

+

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

+
+ {{code}} +
+

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

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

Dify 的登录验证码

+

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

+
+ {{code}} +
+

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

+
+ + diff --git a/api/templates/reset_password_mail_template_en-US.html b/api/templates/reset_password_mail_template_en-US.html index ffc558ab66..fa73144ad4 100644 --- a/api/templates/reset_password_mail_template_en-US.html +++ b/api/templates/reset_password_mail_template_en-US.html @@ -1,72 +1,74 @@ - + - - - + +
-
- Dify Logo -
-
-

Dear {{ to }},

-

We have received a request to reset your password. If you initiated this request, please click the button below to reset your password:

-

Reset Password

-

If you did not request a password reset, please ignore this email and your account will remain secure.

-
- +
+ + Dify Logo +
+

Reset your Dify password

+

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

+
+ {{code}} +
+

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

- + diff --git a/api/templates/reset_password_mail_template_zh-CN.html b/api/templates/reset_password_mail_template_zh-CN.html index b74b23ac3f..88b45420f6 100644 --- a/api/templates/reset_password_mail_template_zh-CN.html +++ b/api/templates/reset_password_mail_template_zh-CN.html @@ -1,72 +1,74 @@ - + - - - + +
-
- Dify Logo -
-
-

尊敬的 {{ to }},

-

我们收到了您关于重置密码的请求。如果是您本人操作,请点击以下按钮重置您的密码:

-

重置密码

-

如果您没有请求重置密码,请忽略此邮件,您的账户信息将保持安全。

-
- +
+ + Dify Logo +
+

重置您的Dify密码

+

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

+
+ {{code}} +
+

如果您没有请求重置密码,请不要担心。您可以安全地忽略此电子邮件。

- + From ccc0ec81789d8e2ab1dc517a0b531fc9f695d2ce Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 29 Aug 2024 14:13:28 +0800 Subject: [PATCH 02/97] feat: add email code login --- api/.env.example | 5 ++ api/configs/feature/__init__.py | 27 ++++++++++- api/controllers/console/auth/error.py | 12 +++++ api/controllers/console/auth/login.py | 67 +++++++++++++++++++++++++++ api/libs/helper.py | 10 ++-- api/services/account_service.py | 33 +++++++++++++ api/tasks/mail_email_code_login.py | 41 ++++++++++++++++ 7 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 api/tasks/mail_email_code_login.py diff --git a/api/.env.example b/api/.env.example index edbb684cc7..22287e6b34 100644 --- a/api/.env.example +++ b/api/.env.example @@ -277,3 +277,8 @@ POSITION_TOOL_EXCLUDES= POSITION_PROVIDER_PINS= POSITION_PROVIDER_INCLUDES= POSITION_PROVIDER_EXCLUDES= + +# Login +EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=1/12 +ALLOW_REGISTER=true +ALLOW_CREATE_WORKSPACE=true \ No newline at end of file diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index f2efa52de3..6b62951b9e 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1,6 +1,15 @@ from typing import Optional -from pydantic import AliasChoices, Field, HttpUrl, NegativeInt, NonNegativeInt, 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 @@ -602,6 +611,21 @@ class PositionConfig(BaseSettings): return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""} +class LoginConfig(BaseSettings): + EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS: PositiveFloat = Field( + description="expiry time in hours for email code login token", + default=1 / 12, + ) + ALLOW_REGISTER: bool = Field( + description="whether to enable register", + default=True, + ) + ALLOW_CREATE_WORKSPACE: bool = Field( + description="whether to enable create workspace", + default=True, + ) + + class FeatureConfig( # place the configs in alphabet order AppExecutionConfig, @@ -627,6 +651,7 @@ class FeatureConfig( WorkflowConfig, WorkspaceConfig, PositionConfig, + LoginConfig, # hosted services config HostedServiceConfig, CeleryBeatConfig, diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index ea23e097d0..a13275a7da 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -29,3 +29,15 @@ class PasswordResetRateLimitExceededError(BaseHTTPException): error_code = "password_reset_rate_limit_exceeded" description = "Password reset rate limit exceeded. Try again later." code = 429 + + +class EmailLoginCodeError(BaseHTTPException): + error_code = "email_login_code_error" + description = "Email login code is invalid or expired." + code = 400 + + +class NotAllowCreateWorkspaceError(BaseHTTPException): + error_code = "workspace_not_found" + description = "Workspace not found." + code = 400 diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 62837af2b9..350276c7bf 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -5,7 +5,15 @@ from flask import request from flask_restful import Resource, reqparse import services +from configs import dify_config +from constants.languages import languages from controllers.console import api +from controllers.console.auth.error import ( + EmailLoginCodeError, + InvalidEmailError, + InvalidTokenError, + NotAllowCreateWorkspaceError, +) from controllers.console.setup import setup_required from libs.helper import email, get_remote_ip from libs.password import valid_password @@ -106,5 +114,64 @@ class ResetPasswordApi(Resource): return {"result": "success"} +class EmailCodeLoginSendEmailApi(Resource): + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=str, required=True, location="json") + args = parser.parse_args() + + account = AccountService.get_user_through_email(args["email"]) + if account is None: + raise InvalidEmailError() + + token = AccountService.send_email_code_login_email(account) + return {"result": "success", "data": token} + + +class EmailCodeLoginApi(Resource): + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=str, required=True, location="json") + parser.add_argument("code", type=str, required=True, location="json") + parser.add_argument("token", type=str, required=True, location="json") + args = parser.parse_args() + + user_email = args["email"] + + token_data = AccountService.get_email_code_login_data(args["token"]) + if token_data is None: + raise InvalidTokenError() + + if token_data["email"] != args["email"]: + raise InvalidEmailError() + + if token_data["code"] != args["code"]: + raise EmailLoginCodeError() + + AccountService.revoke_email_code_login_token(args["token"]) + account = AccountService.get_user_through_email(user_email) + if account is None: + # through environment variable, control whether to allow user to register and create workspace + if dify_config.ALLOW_REGISTER: + account = AccountService.create_account( + email=user_email, name=user_email, interface_language=languages[0] + ) + else: + raise InvalidEmailError() + if dify_config.ALLOW_CREATE_WORKSPACE: + TenantService.create_owner_tenant_if_not_exist(account=account) + else: + raise NotAllowCreateWorkspaceError() + + else: + token = AccountService.login(account, ip_address=get_remote_ip(request)) + + return {"result": "success", "data": token} + + api.add_resource(LoginApi, "/login") api.add_resource(LogoutApi, "/logout") +api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") +api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") diff --git a/api/libs/helper.py b/api/libs/helper.py index af0c2dace1..a486bfc872 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -203,7 +203,8 @@ class TokenManager: expiry_hours = current_app.config[f"{token_type.upper()}_TOKEN_EXPIRY_HOURS"] token_key = cls._get_token_key(token, token_type) - redis_client.setex(token_key, expiry_hours * 60 * 60, json.dumps(token_data)) + expiry_time = int(expiry_hours * 60 * 60) + redis_client.setex(token_key, expiry_time, json.dumps(token_data)) cls._set_current_token_for_account(account.id, token, token_type, expiry_hours) return token @@ -234,9 +235,12 @@ class TokenManager: return current_token @classmethod - def _set_current_token_for_account(cls, account_id: str, token: str, token_type: str, expiry_hours: int): + def _set_current_token_for_account( + cls, account_id: str, token: str, token_type: str, expiry_hours: Union[int, float] + ): key = cls._get_account_token_key(account_id, token_type) - redis_client.setex(key, expiry_hours * 60 * 60, token) + expiry_time = int(expiry_hours * 60 * 60) + redis_client.setex(key, expiry_time, token) @classmethod def _get_account_token_key(cls, account_id: str, token_type: str) -> str: diff --git a/api/services/account_service.py b/api/services/account_service.py index cd501c9792..76e1cc64a8 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -1,5 +1,6 @@ import base64 import logging +import random import secrets import uuid from datetime import datetime, timedelta, timezone @@ -34,6 +35,7 @@ from services.errors.account import ( RoleAlreadyAssignedError, TenantNotFound, ) +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 @@ -246,6 +248,37 @@ class AccountService: def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]: return TokenManager.get_token_data(token, "reset_password") + @classmethod + def send_email_code_login_email(cls, account: Account): + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + token = TokenManager.generate_token(account, "email_code_login", {"code": code}) + send_email_code_login_mail_task.delay( + language=account.interface_language, + to=account.email, + code=code, + ) + + return token + + @classmethod + def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]: + return TokenManager.get_token_data(token, "email_code_login") + + @classmethod + def revoke_email_code_login_token(cls, token: str): + TokenManager.revoke_token(token, "email_code_login") + + @classmethod + def get_user_through_email(cls, email: str): + account = db.session.query(Account).filter(Account.email == email).first() + if not account: + return None + + if account.status in [AccountStatus.BANNED.value, AccountStatus.CLOSED.value]: + raise Unauthorized("Account is banned or closed.") + + return account + def _get_login_cache_key(*, account_id: str, token: str): return f"account_login:{account_id}:{token}" diff --git a/api/tasks/mail_email_code_login.py b/api/tasks/mail_email_code_login.py new file mode 100644 index 0000000000..d78fc2b891 --- /dev/null +++ b/api/tasks/mail_email_code_login.py @@ -0,0 +1,41 @@ +import logging +import time + +import click +from celery import shared_task +from flask import render_template + +from extensions.ext_mail import mail + + +@shared_task(queue="mail") +def send_email_code_login_mail_task(language: str, to: str, code: str): + """ + Async Send email code login mail + :param language: Language in which the email should be sent (e.g., 'en', 'zh') + :param to: Recipient email address + :param code: Email code to be included in the email + """ + if not mail.is_inited(): + return + + logging.info(click.style("Start email code login mail to {}".format(to), fg="green")) + start_at = time.perf_counter() + + # send email code login mail using different languages + try: + if language == "zh-Hans": + html_content = render_template("email_code_login_mail_template_zh-CN.html", to=to, code=code) + mail.send(to=to, subject="邮箱验证码", html=html_content) + else: + html_content = render_template("email_code_login_mail_template_en-US.html", to=to, code=code) + mail.send(to=to, subject="Email Code", html=html_content) + + end_at = time.perf_counter() + logging.info( + click.style( + "Send email code login mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green" + ) + ) + except Exception: + logging.exception("Send email code login mail to {} failed".format(to)) From da684ebfaaa53b75fa490390f6ef6e713e1554c6 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 29 Aug 2024 15:15:48 +0800 Subject: [PATCH 03/97] feat: update register logic --- api/controllers/console/auth/error.py | 6 --- api/controllers/console/auth/login.py | 56 ++++++++++++++++----------- api/libs/helper.py | 28 ++++++++++---- api/services/account_service.py | 43 ++++++++++++++++---- 4 files changed, 89 insertions(+), 44 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index a13275a7da..1c85035d25 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -35,9 +35,3 @@ class EmailLoginCodeError(BaseHTTPException): error_code = "email_login_code_error" description = "Email login code is invalid or expired." code = 400 - - -class NotAllowCreateWorkspaceError(BaseHTTPException): - error_code = "workspace_not_found" - description = "Workspace not found." - code = 400 diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 350276c7bf..6e439863b8 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -12,7 +12,6 @@ from controllers.console.auth.error import ( EmailLoginCodeError, InvalidEmailError, InvalidTokenError, - NotAllowCreateWorkspaceError, ) from controllers.console.setup import setup_required from libs.helper import email, get_remote_ip @@ -33,8 +32,6 @@ class LoginApi(Resource): parser.add_argument("remember_me", type=bool, required=False, default=False, location="json") args = parser.parse_args() - # todo: Verify the recaptcha - try: account = AccountService.authenticate(args["email"], args["password"]) except services.errors.account.AccountLoginError as e: @@ -63,12 +60,31 @@ class LogoutApi(Resource): return {"result": "success"} +class ResetPasswordSendEmailApi(Resource): + @setup_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=email, required=True, location="json") + args = parser.parse_args() + + account = AccountService.get_user_through_email(args["email"]) + if account is None: + if dify_config.ALLOW_REGISTER: + token = AccountService.send_reset_password_email(email=args["email"]) + else: + raise InvalidEmailError() + else: + token = AccountService.send_reset_password_email(account=account) + + return {"result": "success", "data": token} + + class ResetPasswordApi(Resource): @setup_required def get(self): - # parser = reqparse.RequestParser() - # parser.add_argument('email', type=email, required=True, location='json') - # args = parser.parse_args() + parser = reqparse.RequestParser() + parser.add_argument("email", type=email, required=True, location="json") + args = parser.parse_args() # import mailchimp_transactional as MailchimpTransactional # from mailchimp_transactional.api_client import ApiClientError @@ -123,9 +139,13 @@ class EmailCodeLoginSendEmailApi(Resource): account = AccountService.get_user_through_email(args["email"]) if account is None: - raise InvalidEmailError() + if dify_config.ALLOW_REGISTER: + token = AccountService.send_email_code_login_email(email=args["email"]) + else: + raise InvalidEmailError() + else: + token = AccountService.send_email_code_login_email(account=account) - token = AccountService.send_email_code_login_email(account) return {"result": "success", "data": token} @@ -153,25 +173,17 @@ class EmailCodeLoginApi(Resource): AccountService.revoke_email_code_login_token(args["token"]) account = AccountService.get_user_through_email(user_email) if account is None: - # through environment variable, control whether to allow user to register and create workspace - if dify_config.ALLOW_REGISTER: - account = AccountService.create_account( - email=user_email, name=user_email, interface_language=languages[0] - ) - else: - raise InvalidEmailError() - if dify_config.ALLOW_CREATE_WORKSPACE: - TenantService.create_owner_tenant_if_not_exist(account=account) - else: - raise NotAllowCreateWorkspaceError() + account = AccountService.create_user_through_env( + email=user_email, name=user_email, interface_language=languages[0] + ) - else: - token = AccountService.login(account, ip_address=get_remote_ip(request)) + token = AccountService.login(account, ip_address=get_remote_ip(request)) - return {"result": "success", "data": token} + return {"result": "success", "data": token} api.add_resource(LoginApi, "/login") api.add_resource(LogoutApi, "/logout") api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") +api.add_resource(ResetPasswordApi, "/reset-password") diff --git a/api/libs/helper.py b/api/libs/helper.py index a486bfc872..b39e83dc03 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -189,15 +189,25 @@ def compact_generate_response(response: Union[dict, RateLimitGenerator]) -> Resp class TokenManager: @classmethod - def generate_token(cls, account: Account, token_type: str, additional_data: dict = None) -> str: - old_token = cls._get_current_token_for_account(account.id, token_type) - if old_token: - if isinstance(old_token, bytes): - old_token = old_token.decode("utf-8") - cls.revoke_token(old_token, token_type) + def generate_token( + cls, token_type: str, account: Optional[Account] = None, email: Optional[str] = None, + additional_data: dict = None + ) -> str: + if account is None and email is None: + raise ValueError("Account or email must be provided") + + account_id = account.id if account else None + account_email = account.email if account else email + + if account_id: + old_token = cls._get_current_token_for_account(account_id, token_type) + if old_token: + if isinstance(old_token, bytes): + old_token = old_token.decode("utf-8") + cls.revoke_token(old_token, token_type) token = str(uuid.uuid4()) - token_data = {"account_id": account.id, "email": account.email, "token_type": token_type} + token_data = {"account_id": account_id, "email": account_email, "token_type": token_type} if additional_data: token_data.update(additional_data) @@ -206,7 +216,9 @@ class TokenManager: expiry_time = int(expiry_hours * 60 * 60) redis_client.setex(token_key, expiry_time, json.dumps(token_data)) - cls._set_current_token_for_account(account.id, token, token_type, expiry_hours) + if account_id: + cls._set_current_token_for_account(account.id, token, token_type, expiry_hours) + return token @classmethod diff --git a/api/services/account_service.py b/api/services/account_service.py index 76e1cc64a8..e2f30d8ed3 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -158,6 +158,24 @@ class AccountService: db.session.commit() return account + @staticmethod + def create_user_through_env( + email: str, name: str, interface_language: str, password: Optional[str] = None + ) -> Account: + """create account""" + if dify_config.ALLOW_REGISTER: + account = AccountService.create_account( + email=email, name=name, interface_language=interface_language, password=password + ) + else: + raise Unauthorized("Register is not allowed.") + if dify_config.ALLOW_CREATE_WORKSPACE: + TenantService.create_owner_tenant_if_not_exist(account=account) + else: + raise Unauthorized("Create workspace is not allowed.") + + return account + @staticmethod def link_account_integrate(provider: str, open_id: str, account: Account) -> None: """Link account integrate""" @@ -231,13 +249,20 @@ class AccountService: return AccountService.load_user(account_id) @classmethod - def send_reset_password_email(cls, account): + def send_reset_password_email(cls, account: Optional[Account] = None, email: Optional[str] = None): if cls.reset_password_rate_limiter.is_rate_limited(account.email): raise RateLimitExceededError(f"Rate limit exceeded for email: {account.email}. Please try again later.") - token = TokenManager.generate_token(account, "reset_password") - send_reset_password_mail_task.delay(language=account.interface_language, to=account.email, token=token) - cls.reset_password_rate_limiter.increment_rate_limit(account.email) + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + token = TokenManager.generate_token( + account=account, email=email, token_type="reset_password", additional_data={"code": code} + ) + send_reset_password_mail_task.delay( + language=account.interface_language if account else languages[0], + to=account.email if account else email, + code=code, + ) + cls.reset_password_rate_limiter.increment_rate_limit(account.email if account else email) return token @classmethod @@ -249,12 +274,14 @@ class AccountService: return TokenManager.get_token_data(token, "reset_password") @classmethod - def send_email_code_login_email(cls, account: Account): + def send_email_code_login_email(cls, account: Optional[Account] = None, email: Optional[str] = None): code = "".join([str(random.randint(0, 9)) for _ in range(6)]) - token = TokenManager.generate_token(account, "email_code_login", {"code": code}) + token = TokenManager.generate_token( + account=account, email=email, token_type="email_code_login", additional_data={"code": code} + ) send_email_code_login_mail_task.delay( - language=account.interface_language, - to=account.email, + language=account.interface_language if account else languages[0], + to=account.email if account else email, code=code, ) From ede775cb6a24f6d2ec004a157353bc7447557eaf Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 29 Aug 2024 16:51:30 +0800 Subject: [PATCH 04/97] feat: add reset password api --- api/controllers/console/auth/error.py | 6 +- .../console/auth/forgot_password.py | 70 ++++++++++++------- api/controllers/console/auth/login.py | 13 ++-- api/libs/helper.py | 7 +- api/services/account_service.py | 11 +-- api/tasks/mail_reset_password_task.py | 10 ++- .../reset_password_mail_template_en-US.html | 4 +- .../reset_password_mail_template_zh-CN.html | 4 +- 8 files changed, 75 insertions(+), 50 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 1c85035d25..98b4e96beb 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -31,7 +31,7 @@ class PasswordResetRateLimitExceededError(BaseHTTPException): code = 429 -class EmailLoginCodeError(BaseHTTPException): - error_code = "email_login_code_error" - description = "Email login code is invalid or expired." +class EmailCodeError(BaseHTTPException): + error_code = "email_code_error" + description = "Email code is invalid or expired." code = 400 diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 0b01a4906a..8df148538a 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -2,10 +2,14 @@ import base64 import logging import secrets +from flask import request from flask_restful import Resource, reqparse +from configs import dify_config +from constants.languages import languages from controllers.console import api from controllers.console.auth.error import ( + EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError, @@ -13,7 +17,7 @@ from controllers.console.auth.error import ( ) from controllers.console.setup import setup_required from extensions.ext_database import db -from libs.helper import email as email_validate +from libs.helper import email, get_remote_ip from libs.password import hash_password, valid_password from models.account import Account from services.account_service import AccountService @@ -24,42 +28,48 @@ class ForgotPasswordSendEmailApi(Resource): @setup_required def post(self): parser = reqparse.RequestParser() - parser.add_argument("email", type=str, required=True, location="json") + parser.add_argument("email", type=email, required=True, location="json") args = parser.parse_args() - email = args["email"] - - if not email_validate(email): - raise InvalidEmailError() - - account = Account.query.filter_by(email=email).first() - - if account: + account = Account.query.filter_by(email=args["email"]).first() + token = None + if account is None: + if dify_config.ALLOW_REGISTER: + token = AccountService.send_reset_password_email(email=args["email"]) + else: + raise InvalidEmailError() + elif account: try: - AccountService.send_reset_password_email(account=account) + token = AccountService.send_reset_password_email(account=account, email=args["email"]) except RateLimitExceededError: - logging.warning(f"Rate limit exceeded for email: {account.email}") + logging.warning(f"Rate limit exceeded for email: {args["email"]}") raise PasswordResetRateLimitExceededError() - else: - # Return success to avoid revealing email registration status - logging.warning(f"Attempt to reset password for unregistered email: {email}") - return {"result": "success"} + return {"result": "success", "data": token} class ForgotPasswordCheckApi(Resource): @setup_required def post(self): parser = reqparse.RequestParser() + parser.add_argument("email", type=str, required=True, location="json") + parser.add_argument("code", type=str, required=True, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json") args = parser.parse_args() - token = args["token"] - reset_data = AccountService.get_reset_password_data(token) + user_email = args["email"] - if reset_data is None: - return {"is_valid": False, "email": None} - return {"is_valid": True, "email": reset_data.get("email")} + token_data = AccountService.get_reset_password_data(args["token"]) + if token_data is None: + raise InvalidTokenError() + + if user_email != token_data.get("email"): + raise InvalidEmailError() + + if args["code"] != token_data.get("code"): + raise EmailCodeError() + + return {"is_valid": True, "email": token_data.get("email")} class ForgotPasswordResetApi(Resource): @@ -92,11 +102,21 @@ class ForgotPasswordResetApi(Resource): base64_password_hashed = base64.b64encode(password_hashed).decode() account = Account.query.filter_by(email=reset_data.get("email")).first() - account.password = base64_password_hashed - account.password_salt = base64_salt - db.session.commit() + if account: + account.password = base64_password_hashed + account.password_salt = base64_salt + db.session.commit() + else: + account = AccountService.create_user_through_env( + email=reset_data.get("email"), + name=reset_data.get("email"), + password=password_confirm, + interface_language=languages[0], + ) - return {"result": "success"} + token = AccountService.login(account, ip_address=get_remote_ip(request)) + + return {"result": "success", "data": token} api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password") diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 6e439863b8..478d7b9c8a 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,7 +1,7 @@ from typing import cast import flask_login -from flask import request +from flask import redirect, request from flask_restful import Resource, reqparse import services @@ -9,7 +9,7 @@ from configs import dify_config from constants.languages import languages from controllers.console import api from controllers.console.auth.error import ( - EmailLoginCodeError, + EmailCodeError, InvalidEmailError, InvalidTokenError, ) @@ -134,13 +134,14 @@ class EmailCodeLoginSendEmailApi(Resource): @setup_required def post(self): parser = reqparse.RequestParser() - parser.add_argument("email", type=str, required=True, location="json") + parser.add_argument("email", type=email, required=True, location="json") args = parser.parse_args() account = AccountService.get_user_through_email(args["email"]) if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_email_code_login_email(email=args["email"]) + token = AccountService.send_reset_password_email(email=args["email"]) + return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") else: raise InvalidEmailError() else: @@ -168,7 +169,7 @@ class EmailCodeLoginApi(Resource): raise InvalidEmailError() if token_data["code"] != args["code"]: - raise EmailLoginCodeError() + raise EmailCodeError() AccountService.revoke_email_code_login_token(args["token"]) account = AccountService.get_user_through_email(user_email) @@ -186,4 +187,4 @@ api.add_resource(LoginApi, "/login") api.add_resource(LogoutApi, "/logout") api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") -api.add_resource(ResetPasswordApi, "/reset-password") +api.add_resource(ResetPasswordSendEmailApi, "/reset-password") diff --git a/api/libs/helper.py b/api/libs/helper.py index b39e83dc03..7e3c269e3f 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -190,8 +190,11 @@ def compact_generate_response(response: Union[dict, RateLimitGenerator]) -> Resp class TokenManager: @classmethod def generate_token( - cls, token_type: str, account: Optional[Account] = None, email: Optional[str] = None, - additional_data: dict = None + cls, + token_type: str, + account: Optional[Account] = None, + email: Optional[str] = None, + additional_data: dict = None, ) -> str: if account is None and email is None: raise ValueError("Account or email must be provided") diff --git a/api/services/account_service.py b/api/services/account_service.py index e2f30d8ed3..b1912a6e70 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -250,19 +250,22 @@ class AccountService: @classmethod def send_reset_password_email(cls, account: Optional[Account] = None, email: Optional[str] = None): + account_email = account.email if account else email + account_language = account.interface_language if account else languages[0] + if cls.reset_password_rate_limiter.is_rate_limited(account.email): - raise RateLimitExceededError(f"Rate limit exceeded for email: {account.email}. Please try again later.") + raise RateLimitExceededError(f"Rate limit exceeded for email: {account_email}. Please try again later.") code = "".join([str(random.randint(0, 9)) for _ in range(6)]) token = TokenManager.generate_token( account=account, email=email, token_type="reset_password", additional_data={"code": code} ) send_reset_password_mail_task.delay( - language=account.interface_language if account else languages[0], - to=account.email if account else email, + language=account_language, + to=account_email, code=code, ) - cls.reset_password_rate_limiter.increment_rate_limit(account.email if account else email) + cls.reset_password_rate_limiter.increment_rate_limit(account_email) return token @classmethod diff --git a/api/tasks/mail_reset_password_task.py b/api/tasks/mail_reset_password_task.py index cbb78976ca..7a0d2c877b 100644 --- a/api/tasks/mail_reset_password_task.py +++ b/api/tasks/mail_reset_password_task.py @@ -5,17 +5,16 @@ import click from celery import shared_task from flask import render_template -from configs import dify_config from extensions.ext_mail import mail @shared_task(queue="mail") -def send_reset_password_mail_task(language: str, to: str, token: str): +def send_reset_password_mail_task(language: str, to: str, code: str): """ Async Send reset password mail :param language: Language in which the email should be sent (e.g., 'en', 'zh') :param to: Recipient email address - :param token: Reset password token to be included in the email + :param code: Reset password code """ if not mail.is_inited(): return @@ -25,12 +24,11 @@ def send_reset_password_mail_task(language: str, to: str, token: str): # send reset password mail using different languages try: - url = f"{dify_config.CONSOLE_WEB_URL}/forgot-password?token={token}" if language == "zh-Hans": - html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, url=url) + html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code) mail.send(to=to, subject="重置您的 Dify 密码", html=html_content) else: - html_content = render_template("reset_password_mail_template_en-US.html", to=to, url=url) + html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code) mail.send(to=to, subject="Reset Your Dify Password", html=html_content) end_at = time.perf_counter() diff --git a/api/templates/reset_password_mail_template_en-US.html b/api/templates/reset_password_mail_template_en-US.html index fa73144ad4..da8a383ea1 100644 --- a/api/templates/reset_password_mail_template_en-US.html +++ b/api/templates/reset_password_mail_template_en-US.html @@ -63,12 +63,12 @@ Dify Logo -

Reset your Dify password

+

Reset your Dify password

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

{{code}}
-

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

+

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

diff --git a/api/templates/reset_password_mail_template_zh-CN.html b/api/templates/reset_password_mail_template_zh-CN.html index 88b45420f6..190fb091e8 100644 --- a/api/templates/reset_password_mail_template_zh-CN.html +++ b/api/templates/reset_password_mail_template_zh-CN.html @@ -63,12 +63,12 @@ Dify Logo -

重置您的Dify密码

+

重置您的Dify 账户密码

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

{{code}}
-

如果您没有请求重置密码,请不要担心。您可以安全地忽略此电子邮件。

+

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

From 3c747bc6d0f2bb57385958d0a0f219d3f4a95ada Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 29 Aug 2024 16:53:55 +0800 Subject: [PATCH 05/97] feat: add system future config --- api/.env.example | 3 +++ api/configs/feature/__init__.py | 12 ++++++++++++ api/services/feature_service.py | 3 +++ docker/.env.example | 13 ++++++++++--- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api/.env.example b/api/.env.example index 22287e6b34..d4f8d9905a 100644 --- a/api/.env.example +++ b/api/.env.example @@ -279,6 +279,9 @@ POSITION_PROVIDER_INCLUDES= POSITION_PROVIDER_EXCLUDES= # Login +ENABLE_EMAIL_CODE_LOGIN= +ENABLE_EMAIL_PASSWORD_LOGIN= +ENABLE_SOCIAL_OAUTH_LOGIN= EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=1/12 ALLOW_REGISTER=true ALLOW_CREATE_WORKSPACE=true \ No newline at end of file diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 6b62951b9e..d9e7038091 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -612,6 +612,18 @@ class PositionConfig(BaseSettings): class LoginConfig(BaseSettings): + ENABLE_EMAIL_CODE_LOGIN: bool = Field( + description="whether to enable email code login", + default=True, + ) + ENABLE_EMAIL_PASSWORD_LOGIN: bool = Field( + description="whether to enable email password login", + default=True, + ) + ENABLE_SOCIAL_OAUTH_LOGIN: bool = Field( + description="whether to enable github/google oauth login", + default=True, + ) EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS: PositiveFloat = Field( description="expiry time in hours for email code login token", default=1 / 12, diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 4d5812c6c6..72ab9b0e68 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -42,6 +42,9 @@ class SystemFeatureModel(BaseModel): sso_enforced_for_web: bool = False sso_enforced_for_web_protocol: str = "" enable_web_sso_switch_component: bool = False + enable_email_code_login: bool = dify_config.ENABLE_EMAIL_CODE_LOGIN + enable_email_password_login: bool = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN + enable_social_oauth_login: bool = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN class FeatureService: diff --git a/docker/.env.example b/docker/.env.example index 7233c4e671..d090a2fe70 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -507,8 +507,7 @@ INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=1000 INVITE_EXPIRY_HOURS=72 # Reset password token valid time (hours), -# Default: 24. -RESET_PASSWORD_TOKEN_EXPIRY_HOURS=24 +RESET_PASSWORD_TOKEN_EXPIRY_HOURS=1/12 # The sandbox service endpoint. CODE_EXECUTION_ENDPOINT=http://sandbox:8194 @@ -721,4 +720,12 @@ POSITION_TOOL_EXCLUDES= # Example: POSITION_PROVIDER_PINS=openai,openllm POSITION_PROVIDER_PINS= POSITION_PROVIDER_INCLUDES= -POSITION_PROVIDER_EXCLUDES= \ No newline at end of file +POSITION_PROVIDER_EXCLUDES= + +# LoginConfig +ENABLE_EMAIL_CODE_LOGIN= +ENABLE_EMAIL_PASSWORD_LOGIN= +ENABLE_SOCIAL_OAUTH_LOGIN= +EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=1/12 +ALLOW_REGISTER=true +ALLOW_CREATE_WORKSPACE=true \ No newline at end of file From 83bf1c91604817873f5e81a922267152cfda259e Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 29 Aug 2024 17:40:52 +0800 Subject: [PATCH 06/97] fix: email code login redirect --- api/controllers/console/auth/login.py | 63 +++------------------------ 1 file changed, 6 insertions(+), 57 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 478d7b9c8a..6ae2291c3e 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -35,7 +35,11 @@ class LoginApi(Resource): try: account = AccountService.authenticate(args["email"], args["password"]) except services.errors.account.AccountLoginError as e: - return {"code": "unauthorized", "message": str(e)}, 401 + if dify_config.ALLOW_REGISTER: + token = AccountService.send_reset_password_email(email=args["email"]) + return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") + else: + return {"code": "unauthorized", "message": str(e)}, 401 # SELF_HOSTED only have one workspace tenants = TenantService.get_join_tenants(account) @@ -79,57 +83,6 @@ class ResetPasswordSendEmailApi(Resource): return {"result": "success", "data": token} -class ResetPasswordApi(Resource): - @setup_required - def get(self): - parser = reqparse.RequestParser() - parser.add_argument("email", type=email, required=True, location="json") - args = parser.parse_args() - - # import mailchimp_transactional as MailchimpTransactional - # from mailchimp_transactional.api_client import ApiClientError - - # account = {'email': args['email']} - # account = AccountService.get_by_email(args['email']) - # if account is None: - # raise ValueError('Email not found') - # new_password = AccountService.generate_password() - # AccountService.update_password(account, new_password) - - # todo: Send email - # MAILCHIMP_API_KEY = dify_config.MAILCHIMP_TRANSACTIONAL_API_KEY - # mailchimp = MailchimpTransactional(MAILCHIMP_API_KEY) - - # message = { - # 'from_email': 'noreply@example.com', - # 'to': [{'email': account['email']}], - # 'subject': 'Reset your Dify password', - # 'html': """ - #

Dear User,

- #

The Dify team has generated a new password for you, details as follows:

- #

{new_password}

- #

Please change your password to log in as soon as possible.

- #

Regards,

- #

The Dify Team

- # """ - # } - - # response = mailchimp.messages.send({ - # 'message': message, - # # required for transactional email - # ' settings': { - # 'sandbox_mode': dify_config.MAILCHIMP_SANDBOX_MODE, - # }, - # }) - - # Check if MSG was sent - # if response.status_code != 200: - # # handle error - # pass - - return {"result": "success"} - - class EmailCodeLoginSendEmailApi(Resource): @setup_required def post(self): @@ -139,11 +92,7 @@ class EmailCodeLoginSendEmailApi(Resource): account = AccountService.get_user_through_email(args["email"]) if account is None: - if dify_config.ALLOW_REGISTER: - token = AccountService.send_reset_password_email(email=args["email"]) - return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") - else: - raise InvalidEmailError() + token = AccountService.send_email_code_login_email(email=args["email"]) else: token = AccountService.send_email_code_login_email(account=account) From c3b18d00fe8fc68f986e7c4c7b3a93cd9de6235f Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 29 Aug 2024 18:03:31 +0800 Subject: [PATCH 07/97] fix: string error --- api/controllers/console/auth/forgot_password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 8df148538a..632a2f22f5 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -42,7 +42,7 @@ class ForgotPasswordSendEmailApi(Resource): try: token = AccountService.send_reset_password_email(account=account, email=args["email"]) except RateLimitExceededError: - logging.warning(f"Rate limit exceeded for email: {args["email"]}") + logging.warning(f"Rate limit exceeded for email: {args['email']}") raise PasswordResetRateLimitExceededError() return {"result": "success", "data": token} From 865395abba7385d6a4f141c07940715c82c874da Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Fri, 30 Aug 2024 12:36:22 +0800 Subject: [PATCH 08/97] fix: send_reset_password_email limit email error --- 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 b1912a6e70..ecf959eb4b 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -253,7 +253,7 @@ class AccountService: account_email = account.email if account else email account_language = account.interface_language if account else languages[0] - if cls.reset_password_rate_limiter.is_rate_limited(account.email): + if cls.reset_password_rate_limiter.is_rate_limited(account_email): raise RateLimitExceededError(f"Rate limit exceeded for email: {account_email}. Please try again later.") code = "".join([str(random.randint(0, 9)) for _ in range(6)]) From eadf75ad24bd7a3ab06989a85f385e50a7fcc5bb Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 11:08:36 +0800 Subject: [PATCH 09/97] feat: add login account not found --- api/controllers/console/auth/login.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 6ae2291c3e..7b9cf25cc7 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -35,12 +35,14 @@ class LoginApi(Resource): try: account = AccountService.authenticate(args["email"], args["password"]) except services.errors.account.AccountLoginError as e: - if dify_config.ALLOW_REGISTER: - token = AccountService.send_reset_password_email(email=args["email"]) - return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") - else: + return {"code": "unauthorized", "message": str(e)}, 401 + except services.errors.account.AccountNotFound as e: + if not dify_config.ALLOW_REGISTER: return {"code": "unauthorized", "message": str(e)}, 401 + token = AccountService.send_reset_password_email(email=args["email"]) + return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") + # SELF_HOSTED only have one workspace tenants = TenantService.get_join_tenants(account) if len(tenants) == 0: From 955e2871f40f3c4b37bef36b13ceec53f793f561 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 11:09:40 +0800 Subject: [PATCH 10/97] feat: add oauth account not found --- api/controllers/console/auth/oauth.py | 12 +++++++++--- api/services/account_service.py | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index ae1b49f3ec..cdb454dd31 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -6,6 +6,7 @@ import requests from flask import current_app, redirect, request from flask_restful import Resource +import services from configs import dify_config from constants.languages import languages from extensions.ext_database import db @@ -13,6 +14,7 @@ from libs.helper import get_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService +from services.errors.account import AccountNotFound from .. import api @@ -69,7 +71,10 @@ class OAuthCallback(Resource): logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}") return {"error": "OAuth process failed"}, 400 - account = _generate_account(provider, user_info) + try: + account = _generate_account(provider, user_info) + except services.errors.account.AccountNotFound as e: + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") # Check account status if account.status == AccountStatus.BANNED.value or account.status == AccountStatus.CLOSED.value: return {"error": "Account is banned or closed."}, 403 @@ -99,8 +104,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): # Get account by openid or email. account = _get_account_by_openid_or_email(provider, user_info) - if not account: - # Create account + if not account and dify_config.ALLOW_REGISTER: account_name = user_info.name if user_info.name else "Dify" account = RegisterService.register( email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider @@ -114,6 +118,8 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): interface_language = languages[0] account.interface_language = interface_language db.session.commit() + else: + raise AccountNotFound() # Link account AccountService.link_account_integrate(provider, user_info.id, account) diff --git a/api/services/account_service.py b/api/services/account_service.py index ecf959eb4b..019460261c 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -23,6 +23,7 @@ from models.model import DifySetup from services.errors.account import ( AccountAlreadyInTenantError, AccountLoginError, + AccountNotFound, AccountNotLinkTenantError, AccountRegisterError, CannotOperateSelfError, @@ -92,7 +93,7 @@ class AccountService: account = Account.query.filter_by(email=email).first() if not account: - raise AccountLoginError("Invalid email or password.") + raise AccountNotFound() if account.status == AccountStatus.BANNED.value or account.status == AccountStatus.CLOSED.value: raise AccountLoginError("Account is banned or closed.") @@ -330,6 +331,9 @@ class TenantService: @staticmethod def create_owner_tenant_if_not_exist(account: Account): """Create owner tenant if not exist""" + if not dify_config.ALLOW_CREATE_WORKSPACE: + raise Unauthorized("Create workspace is not allowed.") + available_ta = ( TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first() ) From 8a014bdda44868475f4726f9518019f0c6372b31 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 14:11:21 +0800 Subject: [PATCH 11/97] feat: update invite workspace service --- api/controllers/console/auth/activate.py | 16 ++++++++++++--- .../console/auth/forgot_password.py | 2 +- api/controllers/console/auth/login.py | 2 +- api/services/account_service.py | 20 +++++++++---------- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 8ba6b53e7e..3dd0d3b23b 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -10,7 +10,7 @@ from controllers.console.error import AlreadyActivateError from extensions.ext_database import db from libs.helper import email, str_len, timezone from libs.password import hash_password, valid_password -from models.account import AccountStatus +from models.account import AccountStatus, Tenant from services.account_service import RegisterService @@ -27,8 +27,18 @@ class ActivateCheckApi(Resource): token = args["token"] invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token) - - return {"is_valid": invitation is not None, "workspace_name": invitation["tenant"].name if invitation else None} + if invitation: + data = invitation.get("data", {}) + tenant: Tenant = invitation.get("tenant") + workspace_name = tenant.name if tenant else "Unknown Workspace" + workspace_id = tenant.id if tenant else "Unknown Workspace ID" + invitee_email = data.get("email", "Unknown Email") + return { + "is_valid": invitation is not None, + "data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email} + } + else: + return {"is_valid": False} class ActivateApi(Resource): diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 632a2f22f5..e3826b6ef6 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -107,7 +107,7 @@ class ForgotPasswordResetApi(Resource): account.password_salt = base64_salt db.session.commit() else: - account = AccountService.create_user_through_env( + account = AccountService.create_account_and_tenant( email=reset_data.get("email"), name=reset_data.get("email"), password=password_confirm, diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 7b9cf25cc7..cc00793711 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -125,7 +125,7 @@ class EmailCodeLoginApi(Resource): AccountService.revoke_email_code_login_token(args["token"]) account = AccountService.get_user_through_email(user_email) if account is None: - account = AccountService.create_user_through_env( + account = AccountService.create_account_and_tenant( email=user_email, name=user_email, interface_language=languages[0] ) diff --git a/api/services/account_service.py b/api/services/account_service.py index 019460261c..ca80f057ba 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -133,6 +133,8 @@ class AccountService: email: str, name: str, interface_language: str, password: Optional[str] = None, interface_theme: str = "light" ) -> Account: """create account""" + if not dify_config.ALLOW_REGISTER: + raise Unauthorized("Register is not allowed.") account = Account() account.email = email account.name = name @@ -160,20 +162,14 @@ class AccountService: return account @staticmethod - def create_user_through_env( + def create_account_and_tenant( email: str, name: str, interface_language: str, password: Optional[str] = None ) -> Account: """create account""" - if dify_config.ALLOW_REGISTER: - account = AccountService.create_account( - email=email, name=name, interface_language=interface_language, password=password - ) - else: - raise Unauthorized("Register is not allowed.") - if dify_config.ALLOW_CREATE_WORKSPACE: - TenantService.create_owner_tenant_if_not_exist(account=account) - else: - raise Unauthorized("Create workspace is not allowed.") + account = AccountService.create_account( + email=email, name=name, interface_language=interface_language, password=password + ) + TenantService.create_owner_tenant_if_not_exist(account=account) return account @@ -319,6 +315,8 @@ class TenantService: @staticmethod def create_tenant(name: str) -> Tenant: """Create tenant""" + if not dify_config.ALLOW_CREATE_WORKSPACE: + raise Unauthorized("Create workspace is not allowed.") tenant = Tenant(name=name) db.session.add(tenant) From 4938e823572af403e11fa001b63cd6464e99a9d4 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 14:38:28 +0800 Subject: [PATCH 12/97] feat: remove activate password param --- api/controllers/console/auth/activate.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 3dd0d3b23b..4a86519a26 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -48,7 +48,6 @@ class ActivateApi(Resource): parser.add_argument("email", type=email, required=False, nullable=True, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json") parser.add_argument("name", type=str_len(30), required=True, nullable=False, location="json") - parser.add_argument("password", type=valid_password, required=True, nullable=False, location="json") parser.add_argument( "interface_language", type=supported_language, required=True, nullable=False, location="json" ) @@ -64,15 +63,6 @@ class ActivateApi(Resource): account = invitation["account"] account.name = args["name"] - # generate password salt - salt = secrets.token_bytes(16) - base64_salt = base64.b64encode(salt).decode() - - # encrypt password with salt - password_hashed = hash_password(args["password"], salt) - base64_password_hashed = base64.b64encode(password_hashed).decode() - account.password = base64_password_hashed - account.password_salt = base64_salt account.interface_language = args["interface_language"] account.timezone = args["timezone"] account.interface_theme = "light" From cbdbfb844dd51e248498c0ee757a5d1bc087b6f0 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 14:42:24 +0800 Subject: [PATCH 13/97] feat: reformat --- api/controllers/console/auth/activate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 4a86519a26..b86bc64aca 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -1,6 +1,4 @@ -import base64 import datetime -import secrets from flask_restful import Resource, reqparse @@ -9,7 +7,6 @@ from controllers.console import api from controllers.console.error import AlreadyActivateError from extensions.ext_database import db from libs.helper import email, str_len, timezone -from libs.password import hash_password, valid_password from models.account import AccountStatus, Tenant from services.account_service import RegisterService @@ -35,7 +32,7 @@ class ActivateCheckApi(Resource): invitee_email = data.get("email", "Unknown Email") return { "is_valid": invitation is not None, - "data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email} + "data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email}, } else: return {"is_valid": False} From c84f00403528a5e49b8651ce653e4182801c01c7 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 14:50:45 +0800 Subject: [PATCH 14/97] feat: add oauth invite redict --- api/controllers/console/auth/oauth.py | 14 ++++++++++++-- api/libs/oauth.py | 9 +++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index cdb454dd31..fe7d9757f1 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -43,7 +43,7 @@ def get_oauth_providers(): class OAuthLogin(Resource): - def get(self, provider: str): + def get(self, provider: str, invite_toke: Optional[str] = None): OAUTH_PROVIDERS = get_oauth_providers() with current_app.app_context(): oauth_provider = OAUTH_PROVIDERS.get(provider) @@ -51,7 +51,7 @@ class OAuthLogin(Resource): if not oauth_provider: return {"error": "Invalid provider"}, 400 - auth_url = oauth_provider.get_authorization_url() + auth_url = oauth_provider.get_authorization_url(invite_toke) return redirect(auth_url) @@ -64,6 +64,11 @@ class OAuthCallback(Resource): return {"error": "Invalid provider"}, 400 code = request.args.get("code") + state = request.args.get("state") + invite_token = None + if state: + invite_token = state + try: token = oauth_provider.get_access_token(code) user_info = oauth_provider.get_user_info(token) @@ -71,6 +76,11 @@ class OAuthCallback(Resource): logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}") return {"error": "OAuth process failed"}, 400 + if invite_token: + return redirect( + f"{dify_config.CONSOLE_WEB_URL}/invite-settings?invite_token={invite_token}" + ) + try: account = _generate_account(provider, user_info) except services.errors.account.AccountNotFound as e: diff --git a/api/libs/oauth.py b/api/libs/oauth.py index d8ce1a1e66..6b6919de24 100644 --- a/api/libs/oauth.py +++ b/api/libs/oauth.py @@ -1,5 +1,6 @@ import urllib.parse from dataclasses import dataclass +from typing import Optional import requests @@ -40,12 +41,14 @@ class GitHubOAuth(OAuth): _USER_INFO_URL = "https://api.github.com/user" _EMAIL_INFO_URL = "https://api.github.com/user/emails" - def get_authorization_url(self): + def get_authorization_url(self, invite_token: Optional[str] = None): params = { "client_id": self.client_id, "redirect_uri": self.redirect_uri, "scope": "user:email", # Request only basic user information } + if invite_token: + params["state"] = invite_token return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}" def get_access_token(self, code: str): @@ -90,13 +93,15 @@ class GoogleOAuth(OAuth): _TOKEN_URL = "https://oauth2.googleapis.com/token" _USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo" - def get_authorization_url(self): + def get_authorization_url(self, invite_token: Optional[str] = None): params = { "client_id": self.client_id, "response_type": "code", "redirect_uri": self.redirect_uri, "scope": "openid email", } + if invite_token: + params["state"] = invite_token return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}" def get_access_token(self, code: str): From 2c7cb5498de8b003245f3fa0251313db85840225 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 15:24:13 +0800 Subject: [PATCH 15/97] fix: oauth AccountNotFound --- api/controllers/console/auth/oauth.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index fe7d9757f1..c62105a41f 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -43,7 +43,8 @@ def get_oauth_providers(): class OAuthLogin(Resource): - def get(self, provider: str, invite_toke: Optional[str] = None): + def get(self, provider: str): + invite_token = request.args.get('invite_token') or None OAUTH_PROVIDERS = get_oauth_providers() with current_app.app_context(): oauth_provider = OAUTH_PROVIDERS.get(provider) @@ -51,7 +52,7 @@ class OAuthLogin(Resource): if not oauth_provider: return {"error": "Invalid provider"}, 400 - auth_url = oauth_provider.get_authorization_url(invite_toke) + auth_url = oauth_provider.get_authorization_url(invite_token) return redirect(auth_url) @@ -114,7 +115,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): # Get account by openid or email. account = _get_account_by_openid_or_email(provider, user_info) - if not account and dify_config.ALLOW_REGISTER: + if not account: account_name = user_info.name if user_info.name else "Dify" account = RegisterService.register( email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider @@ -128,8 +129,6 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): interface_language = languages[0] account.interface_language = interface_language db.session.commit() - else: - raise AccountNotFound() # Link account AccountService.link_account_integrate(provider, user_info.id, account) From 910f9b73c6e9ef196a06affa18b272cb4536ff88 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 15:25:24 +0800 Subject: [PATCH 16/97] feat: remove redict signin --- api/controllers/console/auth/oauth.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index c62105a41f..dc89fa2f9e 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -14,7 +14,6 @@ from libs.helper import get_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService -from services.errors.account import AccountNotFound from .. import api @@ -44,7 +43,7 @@ def get_oauth_providers(): class OAuthLogin(Resource): def get(self, provider: str): - invite_token = request.args.get('invite_token') or None + invite_token = request.args.get("invite_token") or None OAUTH_PROVIDERS = get_oauth_providers() with current_app.app_context(): oauth_provider = OAUTH_PROVIDERS.get(provider) @@ -78,9 +77,7 @@ class OAuthCallback(Resource): return {"error": "OAuth process failed"}, 400 if invite_token: - return redirect( - f"{dify_config.CONSOLE_WEB_URL}/invite-settings?invite_token={invite_token}" - ) + return redirect(f"{dify_config.CONSOLE_WEB_URL}/invite-settings?invite_token={invite_token}") try: account = _generate_account(provider, user_info) From 0da352ae844f4a273bbb58d193a5dc828dcdfc8f Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 15:40:47 +0800 Subject: [PATCH 17/97] fix: get_authorization_url invite token --- api/controllers/console/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index dc89fa2f9e..fe64b322d0 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -51,7 +51,7 @@ class OAuthLogin(Resource): if not oauth_provider: return {"error": "Invalid provider"}, 400 - auth_url = oauth_provider.get_authorization_url(invite_token) + auth_url = oauth_provider.get_authorization_url(invite_token=invite_token) return redirect(auth_url) From f3429953f38031e2243b8305b5f3597a26dabe73 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 16:03:44 +0800 Subject: [PATCH 18/97] feat: remove extra judgement --- api/controllers/console/auth/oauth.py | 10 +++++++--- api/services/account_service.py | 3 --- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index fe64b322d0..10308943fd 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -5,8 +5,8 @@ from typing import Optional import requests from flask import current_app, redirect, request from flask_restful import Resource +from werkzeug.exceptions import Unauthorized -import services from configs import dify_config from constants.languages import languages from extensions.ext_database import db @@ -81,8 +81,9 @@ class OAuthCallback(Resource): try: account = _generate_account(provider, user_info) - except services.errors.account.AccountNotFound as e: + except Unauthorized: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") + # Check account status if account.status == AccountStatus.BANNED.value or account.status == AccountStatus.CLOSED.value: return {"error": "Account is banned or closed."}, 403 @@ -92,7 +93,10 @@ class OAuthCallback(Resource): account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) db.session.commit() - TenantService.create_owner_tenant_if_not_exist(account) + try: + TenantService.create_owner_tenant_if_not_exist(account) + except Unauthorized: + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") token = AccountService.login(account, ip_address=get_remote_ip(request)) diff --git a/api/services/account_service.py b/api/services/account_service.py index ca80f057ba..2abc2d33a5 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -329,9 +329,6 @@ class TenantService: @staticmethod def create_owner_tenant_if_not_exist(account: Account): """Create owner tenant if not exist""" - if not dify_config.ALLOW_CREATE_WORKSPACE: - raise Unauthorized("Create workspace is not allowed.") - available_ta = ( TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first() ) From b290dcaa0f61d5e9565e4c8918f612b0e0ad1cbb Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 17:42:31 +0800 Subject: [PATCH 19/97] feat: update oauth invite_token redirect url --- api/controllers/console/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 10308943fd..dbe3b0fd23 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -77,7 +77,7 @@ class OAuthCallback(Resource): return {"error": "OAuth process failed"}, 400 if invite_token: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/invite-settings?invite_token={invite_token}") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}") try: account = _generate_account(provider, user_info) From 7136714cfa78d6633a625a87f9dc701316a5bd60 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 17:42:58 +0800 Subject: [PATCH 20/97] feat: add activate token --- api/controllers/console/auth/activate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index b86bc64aca..acde948a81 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -1,14 +1,15 @@ import datetime +from flask import request from flask_restful import Resource, reqparse from constants.languages import supported_language from controllers.console import api from controllers.console.error import AlreadyActivateError from extensions.ext_database import db -from libs.helper import email, str_len, timezone +from libs.helper import email, get_remote_ip, str_len, timezone from models.account import AccountStatus, Tenant -from services.account_service import RegisterService +from services.account_service import AccountService, RegisterService class ActivateCheckApi(Resource): @@ -67,7 +68,9 @@ class ActivateApi(Resource): account.initialized_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) db.session.commit() - return {"result": "success"} + token = AccountService.login(account, ip_address=get_remote_ip(request)) + + return {"result": "success", "data": token} api.add_resource(ActivateCheckApi, "/activate/check") From 7e48aa64022668d4a06869def05204331fb7b865 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 2 Sep 2024 18:04:11 +0800 Subject: [PATCH 21/97] feat: add invitation email judgment --- api/controllers/console/auth/oauth.py | 5 +++++ api/services/account_service.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index dbe3b0fd23..37f4e1bd20 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -77,6 +77,11 @@ class OAuthCallback(Resource): return {"error": "OAuth process failed"}, 400 if invite_token: + invitation = RegisterService._get_invitation_by_token(token=invite_token) + if invitation: + invitation_email = invitation.get("email", None) + if invitation_email != user_info.email: + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=InvalidToken") return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}") try: diff --git a/api/services/account_service.py b/api/services/account_service.py index 2abc2d33a5..de596e929d 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -725,7 +725,9 @@ class RegisterService: } @classmethod - def _get_invitation_by_token(cls, token: str, workspace_id: str, email: str) -> Optional[dict[str, str]]: + def _get_invitation_by_token( + cls, token: str, workspace_id: Optional[str] = None, email: Optional[str] = None + ) -> Optional[dict[str, str]]: if workspace_id is not None and email is not None: email_hash = sha256(email.encode()).hexdigest() cache_key = f"member_invite_token:{workspace_id}, {email_hash}:{token}" From 0eefd3a2ce86014c6e8e34c9d4286e3acf71e716 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 11:55:54 +0800 Subject: [PATCH 22/97] feat: change EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS --- api/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/.env.example b/api/.env.example index d4f8d9905a..dcb523862c 100644 --- a/api/.env.example +++ b/api/.env.example @@ -282,6 +282,6 @@ POSITION_PROVIDER_EXCLUDES= ENABLE_EMAIL_CODE_LOGIN= ENABLE_EMAIL_PASSWORD_LOGIN= ENABLE_SOCIAL_OAUTH_LOGIN= -EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=1/12 +EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=0.0833 ALLOW_REGISTER=true ALLOW_CREATE_WORKSPACE=true \ No newline at end of file From 943259c75e862c5207b507c584a0d1ec0c903649 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 15:36:01 +0800 Subject: [PATCH 23/97] feat: add AccountPasswordError --- api/services/errors/account.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/services/errors/account.py b/api/services/errors/account.py index cae31c5066..ac1551716d 100644 --- a/api/services/errors/account.py +++ b/api/services/errors/account.py @@ -13,6 +13,10 @@ class AccountLoginError(BaseServiceError): pass +class AccountPasswordError(BaseServiceError): + pass + + class AccountNotLinkTenantError(BaseServiceError): pass From d3199137723370ddf0546f5e787b5e19036b9cdf Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 15:37:16 +0800 Subject: [PATCH 24/97] feat: update invite workspace member email password login logic --- api/controllers/console/auth/login.py | 19 +++++++++++++------ api/services/account_service.py | 14 ++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index cc00793711..75c9bf6a60 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -12,7 +12,9 @@ from controllers.console.auth.error import ( EmailCodeError, InvalidEmailError, InvalidTokenError, + PasswordMismatchError, ) +from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegister from controllers.console.setup import setup_required from libs.helper import email, get_remote_ip from libs.password import valid_password @@ -34,11 +36,13 @@ class LoginApi(Resource): try: account = AccountService.authenticate(args["email"], args["password"]) - except services.errors.account.AccountLoginError as e: - return {"code": "unauthorized", "message": str(e)}, 401 - except services.errors.account.AccountNotFound as e: + except services.errors.account.AccountLoginError: + raise NotAllowedRegister() + except services.errors.account.AccountPasswordError: + raise PasswordMismatchError() + except services.errors.account.AccountNotFound: if not dify_config.ALLOW_REGISTER: - return {"code": "unauthorized", "message": str(e)}, 401 + raise NotAllowedCreateWorkspace() token = AccountService.send_reset_password_email(email=args["email"]) return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") @@ -78,7 +82,7 @@ class ResetPasswordSendEmailApi(Resource): if dify_config.ALLOW_REGISTER: token = AccountService.send_reset_password_email(email=args["email"]) else: - raise InvalidEmailError() + raise NotAllowedRegister() else: token = AccountService.send_reset_password_email(account=account) @@ -94,7 +98,10 @@ class EmailCodeLoginSendEmailApi(Resource): account = AccountService.get_user_through_email(args["email"]) if account is None: - token = AccountService.send_email_code_login_email(email=args["email"]) + if dify_config.ALLOW_REGISTER: + token = AccountService.send_email_code_login_email(email=args["email"]) + else: + raise NotAllowedRegister() else: token = AccountService.send_email_code_login_email(account=account) diff --git a/api/services/account_service.py b/api/services/account_service.py index de596e929d..cead5e9c9a 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -25,6 +25,7 @@ from services.errors.account import ( AccountLoginError, AccountNotFound, AccountNotLinkTenantError, + AccountPasswordError, AccountRegisterError, CannotOperateSelfError, CurrentPasswordIncorrectError, @@ -98,13 +99,14 @@ class AccountService: if account.status == AccountStatus.BANNED.value or account.status == AccountStatus.CLOSED.value: raise AccountLoginError("Account is banned or closed.") + if account.password is None or not compare_password(password, account.password, account.password_salt): + raise AccountPasswordError("Invalid email or password.") + if account.status == AccountStatus.PENDING.value: account.status = AccountStatus.ACTIVE.value account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None) db.session.commit() - if account.password is None or not compare_password(password, account.password, account.password_salt): - raise AccountLoginError("Invalid email or password.") return account @staticmethod @@ -134,7 +136,9 @@ class AccountService: ) -> Account: """create account""" if not dify_config.ALLOW_REGISTER: - raise Unauthorized("Register is not allowed.") + from controllers.console.error import NotAllowedRegister + + raise NotAllowedRegister() account = Account() account.email = email account.name = name @@ -316,7 +320,9 @@ class TenantService: def create_tenant(name: str) -> Tenant: """Create tenant""" if not dify_config.ALLOW_CREATE_WORKSPACE: - raise Unauthorized("Create workspace is not allowed.") + from controllers.console.error import NotAllowedCreateWorkspace + + raise NotAllowedCreateWorkspace() tenant = Tenant(name=name) db.session.add(tenant) From 78b1aabb67662941b6ee99cacda8d86246840d45 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 15:53:19 +0800 Subject: [PATCH 25/97] feat: add NotAllowedCreateWorkspace and NotAllowedRegister --- api/controllers/console/auth/forgot_password.py | 3 ++- api/controllers/console/error.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index e3826b6ef6..2b12a77a2b 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -15,6 +15,7 @@ from controllers.console.auth.error import ( PasswordMismatchError, PasswordResetRateLimitExceededError, ) +from controllers.console.error import NotAllowedRegister from controllers.console.setup import setup_required from extensions.ext_database import db from libs.helper import email, get_remote_ip @@ -37,7 +38,7 @@ class ForgotPasswordSendEmailApi(Resource): if dify_config.ALLOW_REGISTER: token = AccountService.send_reset_password_email(email=args["email"]) else: - raise InvalidEmailError() + raise NotAllowedRegister() elif account: try: token = AccountService.send_reset_password_email(account=account, email=args["email"]) diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 1c70ea6c59..3cc7bf9603 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -40,3 +40,15 @@ class AlreadyActivateError(BaseHTTPException): error_code = "already_activate" description = "Auth Token is invalid or account already activated, please check again." code = 403 + + +class NotAllowedCreateWorkspace(BaseHTTPException): + error_code = "unauthorized" + description = "Workspace not found, please contact system admin to invite you to join in a workspace." + code = 400 + + +class NotAllowedRegister(BaseHTTPException): + error_code = "unauthorized" + description = "Account not found." + code = 404 From 187d932420ad9c5e6d9d691eb145a71da5722e8c Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 16:11:45 +0800 Subject: [PATCH 26/97] feat: update register invite member logic --- api/services/account_service.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index cead5e9c9a..3fcfc8cf8a 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -604,6 +604,7 @@ class RegisterService: provider: Optional[str] = None, language: Optional[str] = None, status: Optional[AccountStatus] = None, + is_invite_member: Optional[bool] = False, ) -> Account: db.session.begin_nested() """Register account""" @@ -617,12 +618,15 @@ class RegisterService: if open_id is not None or provider is not None: AccountService.link_account_integrate(provider, open_id, account) if dify_config.EDITION != "SELF_HOSTED": - tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + should_create_workspace = not is_invite_member or ( + is_invite_member and dify_config.ALLOW_CREATE_WORKSPACE + ) - TenantService.create_tenant_member(tenant, account, role="owner") - account.current_tenant = tenant - - tenant_was_created.send(tenant) + if should_create_workspace: + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) db.session.commit() except Exception as e: @@ -643,7 +647,9 @@ class RegisterService: TenantService.check_member_permission(tenant, inviter, None, "add") name = email.split("@")[0] - account = cls.register(email=email, name=name, language=language, status=AccountStatus.PENDING) + account = cls.register( + email=email, name=name, language=language, status=AccountStatus.PENDING, is_invite_member=True + ) # Create new tenant member for invited tenant TenantService.create_tenant_member(tenant, account, role) TenantService.switch_tenant(account, tenant.id) From a7b6a241513ee8495a02155a15da554ab1777e6d Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 16:13:41 +0800 Subject: [PATCH 27/97] feat: update docker env example --- docker/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index d090a2fe70..68ce9ddc96 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -726,6 +726,6 @@ POSITION_PROVIDER_EXCLUDES= ENABLE_EMAIL_CODE_LOGIN= ENABLE_EMAIL_PASSWORD_LOGIN= ENABLE_SOCIAL_OAUTH_LOGIN= -EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=1/12 +EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=0.0833 ALLOW_REGISTER=true ALLOW_CREATE_WORKSPACE=true \ No newline at end of file From 1ae966792a8df38ed16617cf4bdcd94bcf930ac8 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 3 Sep 2024 18:23:35 +0800 Subject: [PATCH 28/97] feat: update reset-password redirect url --- 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 75c9bf6a60..055882411c 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -45,7 +45,7 @@ class LoginApi(Resource): raise NotAllowedCreateWorkspace() token = AccountService.send_reset_password_email(email=args["email"]) - return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}&email={args['email']}") # SELF_HOSTED only have one workspace tenants = TenantService.get_join_tenants(account) From 34cd7b9aa07ebc6deb6e890552682edadf5b27d4 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Wed, 4 Sep 2024 10:25:53 +0800 Subject: [PATCH 29/97] feat: change login no count redirect --- api/controllers/console/auth/login.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 055882411c..e1db3775fe 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -45,8 +45,10 @@ class LoginApi(Resource): raise NotAllowedCreateWorkspace() token = AccountService.send_reset_password_email(email=args["email"]) - return redirect(f"{dify_config.CONSOLE_WEB_URL}/reset-password?token={token}&email={args['email']}") - + return redirect( + location=f"{dify_config.CONSOLE_WEB_URL}/reset-password/check-code?token={token}&email={args['email']}", + code=307 + ) # SELF_HOSTED only have one workspace tenants = TenantService.get_join_tenants(account) if len(tenants) == 0: From bd4bf3ff73e69dbe810d7c3dd7d9a983d1ed7d62 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Wed, 4 Sep 2024 11:04:03 +0800 Subject: [PATCH 30/97] feat: change login AccountNotFound response --- api/controllers/console/auth/login.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index e1db3775fe..9f5338d283 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -45,10 +45,7 @@ class LoginApi(Resource): raise NotAllowedCreateWorkspace() token = AccountService.send_reset_password_email(email=args["email"]) - return redirect( - location=f"{dify_config.CONSOLE_WEB_URL}/reset-password/check-code?token={token}&email={args['email']}", - code=307 - ) + return {"result": "fail", "data": token, "message": "account_not_found"} # SELF_HOSTED only have one workspace tenants = TenantService.get_join_tenants(account) if len(tenants) == 0: From 8c05f1c15735fea3f0f568b61723e98a026247e0 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Wed, 4 Sep 2024 11:04:38 +0800 Subject: [PATCH 31/97] feat: reformat --- 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 9f5338d283..8e4f5927d8 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,7 +1,7 @@ from typing import cast import flask_login -from flask import redirect, request +from flask import request from flask_restful import Resource, reqparse import services From a34908d301c5f88fd1e325c0ade37081c5104c21 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 13:40:38 +0800 Subject: [PATCH 32/97] feat: update activate --- api/controllers/console/auth/activate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index acde948a81..e3402329c1 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -27,10 +27,10 @@ class ActivateCheckApi(Resource): invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token) if invitation: data = invitation.get("data", {}) - tenant: Tenant = invitation.get("tenant") - workspace_name = tenant.name if tenant else "Unknown Workspace" - workspace_id = tenant.id if tenant else "Unknown Workspace ID" - invitee_email = data.get("email", "Unknown Email") + tenant: Tenant = invitation.get("tenant", None) + workspace_name = tenant.name if tenant else None + workspace_id = tenant.id if tenant else None + invitee_email = data.get("email") if data else None return { "is_valid": invitation is not None, "data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email}, From d5427b8d7ee7713cd8a0d1b529548b981f930a5c Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 13:48:03 +0800 Subject: [PATCH 33/97] feat: remove self host judgement --- api/services/account_service.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 3fcfc8cf8a..402366b1e2 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -617,16 +617,16 @@ class RegisterService: if open_id is not None or provider is not None: AccountService.link_account_integrate(provider, open_id, account) - if dify_config.EDITION != "SELF_HOSTED": - should_create_workspace = not is_invite_member or ( - is_invite_member and dify_config.ALLOW_CREATE_WORKSPACE - ) + + should_create_workspace = not is_invite_member or ( + is_invite_member and dify_config.ALLOW_CREATE_WORKSPACE + ) - if should_create_workspace: - tenant = TenantService.create_tenant(f"{account.name}'s Workspace") - TenantService.create_tenant_member(tenant, account, role="owner") - account.current_tenant = tenant - tenant_was_created.send(tenant) + if should_create_workspace: + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) db.session.commit() except Exception as e: From 00566af8d1a04ddfeb642f52ff3e6f9e0404b7e8 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 14:37:05 +0800 Subject: [PATCH 34/97] fix: reformat --- api/services/account_service.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 402366b1e2..e63478361d 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -617,10 +617,8 @@ class RegisterService: if open_id is not None or provider is not None: AccountService.link_account_integrate(provider, open_id, account) - - should_create_workspace = not is_invite_member or ( - is_invite_member and dify_config.ALLOW_CREATE_WORKSPACE - ) + + should_create_workspace = not is_invite_member or (is_invite_member and dify_config.ALLOW_CREATE_WORKSPACE) if should_create_workspace: tenant = TenantService.create_tenant(f"{account.name}'s Workspace") From 076ce1151115afa83ac49a9039ad55bd41b117d9 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 14:49:21 +0800 Subject: [PATCH 35/97] feat: add OAuth invite token check --- api/controllers/console/auth/oauth.py | 3 ++- api/services/account_service.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 37f4e1bd20..70fd2f6774 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -76,12 +76,13 @@ class OAuthCallback(Resource): logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}") return {"error": "OAuth process failed"}, 400 - if invite_token: + if invite_token and RegisterService.is_valid_invite_token(invite_token): invitation = RegisterService._get_invitation_by_token(token=invite_token) if invitation: invitation_email = invitation.get("email", None) if invitation_email != user_info.email: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=InvalidToken") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}") try: diff --git a/api/services/account_service.py b/api/services/account_service.py index e63478361d..2f16e34097 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -687,6 +687,11 @@ class RegisterService: redis_client.setex(cls._get_invitation_token_key(token), expiryHours * 60 * 60, json.dumps(invitation_data)) return token + @classmethod + def is_valid_invite_token(cls, token: str) -> bool: + data = redis_client.get(cls._get_invitation_token_key(token)) + return data is not None + @classmethod def revoke_token(cls, workspace_id: str, email: str, token: str): if workspace_id and email: From 3742a277fc3f0ca472c0f9670db603d65f1f3e28 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 14:50:56 +0800 Subject: [PATCH 36/97] fix: code reformat --- api/controllers/console/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 70fd2f6774..7acc05f1da 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -82,7 +82,7 @@ class OAuthCallback(Resource): invitation_email = invitation.get("email", None) if invitation_email != user_info.email: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=InvalidToken") - + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}") try: From 1f8bad9bf6b81408cc9a50e9fbeeb3cc2415cf07 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 16:55:36 +0800 Subject: [PATCH 37/97] feat: update env example --- api/.env.example | 6 +++--- docker/.env.example | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/.env.example b/api/.env.example index dcb523862c..502f641d0c 100644 --- a/api/.env.example +++ b/api/.env.example @@ -279,9 +279,9 @@ POSITION_PROVIDER_INCLUDES= POSITION_PROVIDER_EXCLUDES= # Login -ENABLE_EMAIL_CODE_LOGIN= -ENABLE_EMAIL_PASSWORD_LOGIN= -ENABLE_SOCIAL_OAUTH_LOGIN= +ENABLE_EMAIL_CODE_LOGIN=true +ENABLE_EMAIL_PASSWORD_LOGIN=true +ENABLE_SOCIAL_OAUTH_LOGIN=true EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=0.0833 ALLOW_REGISTER=true ALLOW_CREATE_WORKSPACE=true \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example index 68ce9ddc96..9138360d8b 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -723,9 +723,9 @@ POSITION_PROVIDER_INCLUDES= POSITION_PROVIDER_EXCLUDES= # LoginConfig -ENABLE_EMAIL_CODE_LOGIN= -ENABLE_EMAIL_PASSWORD_LOGIN= -ENABLE_SOCIAL_OAUTH_LOGIN= +ENABLE_EMAIL_CODE_LOGIN=true +ENABLE_EMAIL_PASSWORD_LOGIN=true +ENABLE_SOCIAL_OAUTH_LOGIN=true EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=0.0833 ALLOW_REGISTER=true -ALLOW_CREATE_WORKSPACE=true \ No newline at end of file +ALLOW_CREATE_WORKSPACE=true From b46bb3e7a432082389334536ad8215e4ca3c9fb9 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 17:17:18 +0800 Subject: [PATCH 38/97] feat: add ALLOW_REGISTER judgement in _generate_account --- api/controllers/console/auth/oauth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 7acc05f1da..aba782c181 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -123,6 +123,8 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): account = _get_account_by_openid_or_email(provider, user_info) if not account: + if not dify_config.ALLOW_REGISTER: + raise Unauthorized("Account not found") account_name = user_info.name if user_info.name else "Dify" account = RegisterService.register( email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider From ef988371c724dbecfc379c0bebc21f10a5c21e2c Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 17:27:40 +0800 Subject: [PATCH 39/97] fix: OAuth error when not allow register --- api/controllers/console/auth/login.py | 2 +- api/controllers/console/auth/oauth.py | 3 ++- api/controllers/console/error.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 8e4f5927d8..139c6a06a9 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -42,7 +42,7 @@ class LoginApi(Resource): raise PasswordMismatchError() except services.errors.account.AccountNotFound: if not dify_config.ALLOW_REGISTER: - raise NotAllowedCreateWorkspace() + raise NotAllowedRegister() token = AccountService.send_reset_password_email(email=args["email"]) return {"result": "fail", "data": token, "message": "account_not_found"} diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index aba782c181..e387cf22a3 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -9,6 +9,7 @@ from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import languages +from controllers.console.error import NotAllowedRegister from extensions.ext_database import db from libs.helper import get_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo @@ -124,7 +125,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): if not account: if not dify_config.ALLOW_REGISTER: - raise Unauthorized("Account not found") + raise NotAllowedRegister() account_name = user_info.name if user_info.name else "Dify" account = RegisterService.register( email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 3cc7bf9603..1d1a93a5c2 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -51,4 +51,4 @@ class NotAllowedCreateWorkspace(BaseHTTPException): class NotAllowedRegister(BaseHTTPException): error_code = "unauthorized" description = "Account not found." - code = 404 + code = 400 From 97ecaccf6ee9b1d012e836f2ce8af2ce9245fb99 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 17:28:11 +0800 Subject: [PATCH 40/97] feat: dev/reformat --- 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 139c6a06a9..29bf92de44 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -14,7 +14,7 @@ from controllers.console.auth.error import ( InvalidTokenError, PasswordMismatchError, ) -from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegister +from controllers.console.error import NotAllowedRegister from controllers.console.setup import setup_required from libs.helper import email, get_remote_ip from libs.password import valid_password From 358a5f61d36bf7ce5a73134d2e447cb473161270 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 17:44:50 +0800 Subject: [PATCH 41/97] feat: add OAuthCallback redirect --- api/controllers/console/auth/oauth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index e387cf22a3..2e0eeb2895 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -9,12 +9,12 @@ from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import languages -from controllers.console.error import NotAllowedRegister from extensions.ext_database import db from libs.helper import get_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService +from services.errors.account import AccountNotFound from .. import api @@ -88,7 +88,7 @@ class OAuthCallback(Resource): try: account = _generate_account(provider, user_info) - except Unauthorized: + except AccountNotFound: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") # Check account status @@ -125,7 +125,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): if not account: if not dify_config.ALLOW_REGISTER: - raise NotAllowedRegister() + raise AccountNotFound() account_name = user_info.name if user_info.name else "Dify" account = RegisterService.register( email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider From 4893631d656b6f55f8bee50d1bde4fd2268fb6e3 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 9 Sep 2024 18:19:55 +0800 Subject: [PATCH 42/97] fix: oauth error when not allowed create workspace fix: oauth error when not allowed create workspace --- api/controllers/console/auth/oauth.py | 5 +++++ api/services/account_service.py | 18 ++++++++---------- api/services/errors/workspace.py | 5 +++++ 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 api/services/errors/workspace.py diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 2e0eeb2895..b653c91dfd 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -15,6 +15,7 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService from services.errors.account import AccountNotFound +from services.errors.workspace import WorkSpaceNotAllowedCreateError from .. import api @@ -90,6 +91,10 @@ class OAuthCallback(Resource): account = _generate_account(provider, user_info) except AccountNotFound: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") + except WorkSpaceNotAllowedCreateError: + return redirect( + f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." + ) # Check account status if account.status == AccountStatus.BANNED.value or account.status == AccountStatus.CLOSED.value: diff --git a/api/services/account_service.py b/api/services/account_service.py index 2f16e34097..7cbbc8c428 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -37,6 +37,7 @@ from services.errors.account import ( RoleAlreadyAssignedError, TenantNotFound, ) +from services.errors.workspace import WorkSpaceNotAllowedCreateError 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 @@ -604,7 +605,6 @@ class RegisterService: provider: Optional[str] = None, language: Optional[str] = None, status: Optional[AccountStatus] = None, - is_invite_member: Optional[bool] = False, ) -> Account: db.session.begin_nested() """Register account""" @@ -618,13 +618,13 @@ class RegisterService: if open_id is not None or provider is not None: AccountService.link_account_integrate(provider, open_id, account) - should_create_workspace = not is_invite_member or (is_invite_member and dify_config.ALLOW_CREATE_WORKSPACE) + if not dify_config.ALLOW_CREATE_WORKSPACE: + raise WorkSpaceNotAllowedCreateError() - if should_create_workspace: - tenant = TenantService.create_tenant(f"{account.name}'s Workspace") - TenantService.create_tenant_member(tenant, account, role="owner") - account.current_tenant = tenant - tenant_was_created.send(tenant) + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) db.session.commit() except Exception as e: @@ -645,9 +645,7 @@ class RegisterService: TenantService.check_member_permission(tenant, inviter, None, "add") name = email.split("@")[0] - account = cls.register( - email=email, name=name, language=language, status=AccountStatus.PENDING, is_invite_member=True - ) + account = cls.register(email=email, name=name, language=language, status=AccountStatus.PENDING) # Create new tenant member for invited tenant TenantService.create_tenant_member(tenant, account, role) TenantService.switch_tenant(account, tenant.id) diff --git a/api/services/errors/workspace.py b/api/services/errors/workspace.py new file mode 100644 index 0000000000..600ebad8c1 --- /dev/null +++ b/api/services/errors/workspace.py @@ -0,0 +1,5 @@ +from services.errors.base import BaseServiceError + + +class WorkSpaceNotAllowedCreateError(BaseServiceError): + pass From 3f954e2b9fecc1297b5f89cb13ff53df2d5e42ef Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 10 Sep 2024 10:46:35 +0800 Subject: [PATCH 43/97] feat: register service WorkSpaceNotAllowedCreateError --- api/services/account_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/services/account_service.py b/api/services/account_service.py index 7cbbc8c428..3ee24f0a51 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -627,6 +627,9 @@ class RegisterService: tenant_was_created.send(tenant) db.session.commit() + except WorkSpaceNotAllowedCreateError: + db.session.rollback() + raise except Exception as e: db.session.rollback() logging.error(f"Register failed: {e}") From 57a2534c47775666c64fddbdb329dab3ccd9fc53 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 10 Sep 2024 10:58:55 +0800 Subject: [PATCH 44/97] feat: add workspace not found --- api/controllers/console/auth/oauth.py | 9 ++++++++- api/services/errors/workspace.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index b653c91dfd..03e2318416 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -15,7 +15,7 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService from services.errors.account import AccountNotFound -from services.errors.workspace import WorkSpaceNotAllowedCreateError +from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFound from .. import api @@ -91,6 +91,8 @@ class OAuthCallback(Resource): account = _generate_account(provider, user_info) except AccountNotFound: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") + except WorkSpaceNotFound: + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") except WorkSpaceNotAllowedCreateError: return redirect( f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." @@ -128,6 +130,11 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): # Get account by openid or email. account = _get_account_by_openid_or_email(provider, user_info) + if account: + tenant = TenantService.get_join_tenants(account) + if not tenant: + raise WorkSpaceNotFound() + if not account: if not dify_config.ALLOW_REGISTER: raise AccountNotFound() diff --git a/api/services/errors/workspace.py b/api/services/errors/workspace.py index 600ebad8c1..a379d11124 100644 --- a/api/services/errors/workspace.py +++ b/api/services/errors/workspace.py @@ -3,3 +3,7 @@ from services.errors.base import BaseServiceError class WorkSpaceNotAllowedCreateError(BaseServiceError): pass + + +class WorkSpaceNotFound(BaseServiceError): + pass From 4d1efbef62e8049180a4d5be6c5ff6b254bd05c0 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 10 Sep 2024 11:36:20 +0800 Subject: [PATCH 45/97] feat: add not ALLOW_CREATE_WORKSPACE --- .../console/auth/forgot_password.py | 16 +++++++++-- api/controllers/console/auth/login.py | 28 +++++++++++++++---- api/controllers/console/auth/oauth.py | 9 +++++- api/services/account_service.py | 4 ++- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 2b12a77a2b..36f217087b 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -2,7 +2,7 @@ import base64 import logging import secrets -from flask import request +from flask import redirect, request from flask_restful import Resource, reqparse from configs import dify_config @@ -17,11 +17,12 @@ from controllers.console.auth.error import ( ) from controllers.console.error import NotAllowedRegister from controllers.console.setup import setup_required +from events.tenant_event import tenant_was_created from extensions.ext_database import db from libs.helper import email, get_remote_ip from libs.password import hash_password, valid_password from models.account import Account -from services.account_service import AccountService +from services.account_service import AccountService, TenantService from services.errors.account import RateLimitExceededError @@ -107,6 +108,17 @@ class ForgotPasswordResetApi(Resource): account.password = base64_password_hashed account.password_salt = base64_salt db.session.commit() + tenant = TenantService.get_join_tenants(account) + if not tenant: + if not dify_config.ALLOW_CREATE_WORKSPACE: + return redirect( + f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." + ) + else: + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) else: account = AccountService.create_account_and_tenant( email=reset_data.get("email"), diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 29bf92de44..8c2112e544 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -1,7 +1,7 @@ from typing import cast import flask_login -from flask import request +from flask import redirect, request from flask_restful import Resource, reqparse import services @@ -16,10 +16,12 @@ from controllers.console.auth.error import ( ) from controllers.console.error import NotAllowedRegister from controllers.console.setup import setup_required +from events.tenant_event import tenant_was_created from libs.helper import email, get_remote_ip from libs.password import valid_password from models.account import Account from services.account_service import AccountService, TenantService +from services.errors.workspace import WorkSpaceNotAllowedCreateError class LoginApi(Resource): @@ -130,11 +132,27 @@ class EmailCodeLoginApi(Resource): AccountService.revoke_email_code_login_token(args["token"]) account = AccountService.get_user_through_email(user_email) - if account is None: - account = AccountService.create_account_and_tenant( - email=user_email, name=user_email, interface_language=languages[0] - ) + tenant = TenantService.get_join_tenants(account) + if not tenant: + if not dify_config.ALLOW_CREATE_WORKSPACE: + return redirect( + f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." + ) + else: + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) + if account is None: + try: + account = AccountService.create_account_and_tenant( + email=user_email, name=user_email, interface_language=languages[0] + ) + except WorkSpaceNotAllowedCreateError: + return redirect( + f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." + ) token = AccountService.login(account, ip_address=get_remote_ip(request)) return {"result": "success", "data": token} diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 03e2318416..ff0407d5fb 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -9,6 +9,7 @@ from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import languages +from events.tenant_event import tenant_was_created from extensions.ext_database import db from libs.helper import get_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo @@ -133,7 +134,13 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): if account: tenant = TenantService.get_join_tenants(account) if not tenant: - raise WorkSpaceNotFound() + if not dify_config.ALLOW_CREATE_WORKSPACE: + raise WorkSpaceNotAllowedCreateError() + else: + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) if not account: if not dify_config.ALLOW_REGISTER: diff --git a/api/services/account_service.py b/api/services/account_service.py index 3ee24f0a51..d291e4a6c7 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -334,8 +334,10 @@ class TenantService: return tenant @staticmethod - def create_owner_tenant_if_not_exist(account: Account): + def create_owner_tenant_if_not_exist(account: Account, name: Optional[str] = None): """Create owner tenant if not exist""" + if not dify_config.ALLOW_CREATE_WORKSPACE: + raise WorkSpaceNotAllowedCreateError() available_ta = ( TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first() ) From ca5b29438be2fbd79c205793abaeedc4393b6aae Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 10 Sep 2024 11:51:40 +0800 Subject: [PATCH 46/97] feat: add NotAllowedCreateWorkspace --- api/controllers/console/auth/forgot_password.py | 8 +++----- api/controllers/console/auth/login.py | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 36f217087b..176fe6049f 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -2,7 +2,7 @@ import base64 import logging import secrets -from flask import redirect, request +from flask import request from flask_restful import Resource, reqparse from configs import dify_config @@ -15,7 +15,7 @@ from controllers.console.auth.error import ( PasswordMismatchError, PasswordResetRateLimitExceededError, ) -from controllers.console.error import NotAllowedRegister +from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegister from controllers.console.setup import setup_required from events.tenant_event import tenant_was_created from extensions.ext_database import db @@ -111,9 +111,7 @@ class ForgotPasswordResetApi(Resource): tenant = TenantService.get_join_tenants(account) if not tenant: if not dify_config.ALLOW_CREATE_WORKSPACE: - return redirect( - f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." - ) + return NotAllowedCreateWorkspace() else: tenant = TenantService.create_tenant(f"{account.name}'s Workspace") TenantService.create_tenant_member(tenant, account, role="owner") diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 8c2112e544..7c02a64909 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -14,7 +14,7 @@ from controllers.console.auth.error import ( InvalidTokenError, PasswordMismatchError, ) -from controllers.console.error import NotAllowedRegister +from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegister from controllers.console.setup import setup_required from events.tenant_event import tenant_was_created from libs.helper import email, get_remote_ip @@ -135,9 +135,7 @@ class EmailCodeLoginApi(Resource): tenant = TenantService.get_join_tenants(account) if not tenant: if not dify_config.ALLOW_CREATE_WORKSPACE: - return redirect( - f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." - ) + return NotAllowedCreateWorkspace() else: tenant = TenantService.create_tenant(f"{account.name}'s Workspace") TenantService.create_tenant_member(tenant, account, role="owner") From 148747c3c6ba3378628c98c75ce6180fa7af3a9e Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Tue, 10 Sep 2024 13:47:10 +0800 Subject: [PATCH 47/97] fix: raise --- 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 176fe6049f..1a88d7d5d7 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -111,7 +111,7 @@ class ForgotPasswordResetApi(Resource): tenant = TenantService.get_join_tenants(account) if not tenant: if not dify_config.ALLOW_CREATE_WORKSPACE: - return NotAllowedCreateWorkspace() + raise NotAllowedCreateWorkspace() else: tenant = TenantService.create_tenant(f"{account.name}'s Workspace") TenantService.create_tenant_member(tenant, account, role="owner") diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 7c02a64909..052a207a71 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -135,7 +135,7 @@ class EmailCodeLoginApi(Resource): tenant = TenantService.get_join_tenants(account) if not tenant: if not dify_config.ALLOW_CREATE_WORKSPACE: - return NotAllowedCreateWorkspace() + raise NotAllowedCreateWorkspace() else: tenant = TenantService.create_tenant(f"{account.name}'s Workspace") TenantService.create_tenant_member(tenant, account, role="owner") From 38ea1dd0c65e81640880d78e36295b2c9e1c18ad Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Wed, 11 Sep 2024 10:38:16 +0800 Subject: [PATCH 48/97] feat: update SystemFeatureModel --- api/services/feature_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 72ab9b0e68..6a7eb8c34a 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -45,6 +45,8 @@ class SystemFeatureModel(BaseModel): enable_email_code_login: bool = dify_config.ENABLE_EMAIL_CODE_LOGIN enable_email_password_login: bool = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN enable_social_oauth_login: bool = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN + is_allow_register: bool = dify_config.ALLOW_REGISTER + is_allow_create_workspace: bool = dify_config.ALLOW_CREATE_WORKSPACE class FeatureService: From dc0caab45c4418e12d640f60e8958358e4c23924 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 10:40:44 +0800 Subject: [PATCH 49/97] fix: email code login tenant check --- api/controllers/console/auth/login.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 052a207a71..1daaa44477 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -132,15 +132,16 @@ class EmailCodeLoginApi(Resource): AccountService.revoke_email_code_login_token(args["token"]) account = AccountService.get_user_through_email(user_email) - tenant = TenantService.get_join_tenants(account) - if not tenant: - if not dify_config.ALLOW_CREATE_WORKSPACE: - raise NotAllowedCreateWorkspace() - else: - tenant = TenantService.create_tenant(f"{account.name}'s Workspace") - TenantService.create_tenant_member(tenant, account, role="owner") - account.current_tenant = tenant - tenant_was_created.send(tenant) + if account: + tenant = TenantService.get_join_tenants(account) + if not tenant: + if not dify_config.ALLOW_CREATE_WORKSPACE: + raise NotAllowedCreateWorkspace() + else: + tenant = TenantService.create_tenant(f"{account.name}'s Workspace") + TenantService.create_tenant_member(tenant, account, role="owner") + account.current_tenant = tenant + tenant_was_created.send(tenant) if account is None: try: From 8c2f381d1ae88563b1c72dcb8ee41d5f7ff494a9 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 11:10:02 +0800 Subject: [PATCH 50/97] feat: add create_owner_tenant_if_not_exist WorkSpaceNotAllowedCreateError --- api/controllers/console/auth/oauth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index ff0407d5fb..e446878488 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -112,6 +112,10 @@ class OAuthCallback(Resource): TenantService.create_owner_tenant_if_not_exist(account) except Unauthorized: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") + except WorkSpaceNotAllowedCreateError: + return redirect( + f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found, please contact system admin to invite you to join in a workspace." + ) token = AccountService.login(account, ip_address=get_remote_ip(request)) From edfbcd46ca4216181288d6064ef30fe5045b6961 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 11:32:02 +0800 Subject: [PATCH 51/97] fix: lint error --- api/controllers/console/auth/oauth.py | 4 ++-- api/core/rag/extractor/word_extractor.py | 2 +- api/services/errors/workspace.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index e446878488..0340c8c212 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -16,7 +16,7 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService from services.errors.account import AccountNotFound -from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFound +from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError from .. import api @@ -92,7 +92,7 @@ class OAuthCallback(Resource): account = _generate_account(provider, user_info) except AccountNotFound: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") - except WorkSpaceNotFound: + except WorkSpaceNotFoundError: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") except WorkSpaceNotAllowedCreateError: return redirect( diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index 2db00d161b..dfa02e789e 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -7,7 +7,7 @@ import os import re import tempfile import uuid -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # noqa: N817 from urllib.parse import urlparse import requests diff --git a/api/services/errors/workspace.py b/api/services/errors/workspace.py index a379d11124..714064ffdf 100644 --- a/api/services/errors/workspace.py +++ b/api/services/errors/workspace.py @@ -5,5 +5,5 @@ class WorkSpaceNotAllowedCreateError(BaseServiceError): pass -class WorkSpaceNotFound(BaseServiceError): +class WorkSpaceNotFoundError(BaseServiceError): pass From ce94a61f2f329f567cd58cef531971ef1d23a555 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 11:35:28 +0800 Subject: [PATCH 52/97] fix: N817 CamelCase ElementTree imported as acronym ET --- api/core/rag/extractor/word_extractor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index dfa02e789e..6a06705713 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -7,8 +7,8 @@ import os import re import tempfile import uuid -import xml.etree.ElementTree as ET # noqa: N817 from urllib.parse import urlparse +from xml.etree import ElementTree import requests from docx import Document as DocxDocument @@ -217,7 +217,7 @@ class WordExtractor(BaseExtractor): hyperlinks_url = None if "HYPERLINK" in run.element.xml: try: - xml = ET.XML(run.element.xml) + xml = ElementTree.XML(run.element.xml) x_child = [c for c in xml.iter() if c is not None] for x in x_child: if x_child is None: From dc04431d8a7f22abfded0cc254a883938232f595 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 11:47:36 +0800 Subject: [PATCH 53/97] chore: apply pep8-naming rules for naming --- api/controllers/console/auth/login.py | 2 +- api/controllers/console/auth/oauth.py | 6 +++--- api/services/account_service.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 1daaa44477..2a9e18912a 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -42,7 +42,7 @@ class LoginApi(Resource): raise NotAllowedRegister() except services.errors.account.AccountPasswordError: raise PasswordMismatchError() - except services.errors.account.AccountNotFound: + except services.errors.account.AccountNotFoundError: if not dify_config.ALLOW_REGISTER: raise NotAllowedRegister() diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 0340c8c212..2dd050b30c 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -15,7 +15,7 @@ from libs.helper import get_remote_ip from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo from models.account import Account, AccountStatus from services.account_service import AccountService, RegisterService, TenantService -from services.errors.account import AccountNotFound +from services.errors.account import AccountNotFoundError from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError from .. import api @@ -90,7 +90,7 @@ class OAuthCallback(Resource): try: account = _generate_account(provider, user_info) - except AccountNotFound: + except AccountNotFoundError: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") except WorkSpaceNotFoundError: return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") @@ -148,7 +148,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo): if not account: if not dify_config.ALLOW_REGISTER: - raise AccountNotFound() + raise AccountNotFoundError() account_name = user_info.name if user_info.name else "Dify" account = RegisterService.register( email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider diff --git a/api/services/account_service.py b/api/services/account_service.py index db9e2ed1ad..c42b6d246a 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -23,7 +23,7 @@ from models.model import DifySetup from services.errors.account import ( AccountAlreadyInTenantError, AccountLoginError, - AccountNotFound, + AccountNotFoundError, AccountNotLinkTenantError, AccountPasswordError, AccountRegisterError, @@ -95,7 +95,7 @@ class AccountService: account = Account.query.filter_by(email=email).first() if not account: - raise AccountNotFound() + raise AccountNotFoundError() if account.status == AccountStatus.BANNED.value or account.status == AccountStatus.CLOSED.value: raise AccountLoginError("Account is banned or closed.") From 0b436345736788ed6efe11a329a005618d4d5908 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 13:59:52 +0800 Subject: [PATCH 54/97] chore: remove param --- api/controllers/console/auth/activate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 017f643781..4d2b6fb352 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -47,7 +47,6 @@ class ActivateApi(Resource): parser.add_argument("email", type=email, required=False, nullable=True, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json") parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json") - parser.add_argument("password", type=valid_password, required=True, nullable=False, location="json") parser.add_argument( "interface_language", type=supported_language, required=True, nullable=False, location="json" ) From 753f802d9738728dda1f76475d3aad9df160188b Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 14:00:25 +0800 Subject: [PATCH 55/97] chore: remove not use import --- api/controllers/console/auth/activate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 4d2b6fb352..623443c062 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -8,7 +8,6 @@ from controllers.console import api from controllers.console.error import AlreadyActivateError from extensions.ext_database import db from libs.helper import StrLen, email, get_remote_ip, timezone -from libs.password import valid_password from models.account import AccountStatus, Tenant from services.account_service import AccountService, RegisterService From 6221ae4d387f73cc968cd0cb326e1ea39bd9f514 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 17:08:02 +0800 Subject: [PATCH 56/97] chore: noqa S701 chore: noqa S701 chore: noqa S701 --- api/core/rag/extractor/word_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index 79a24ec212..5d65efd538 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -49,7 +49,7 @@ class WordExtractor(BaseExtractor): self.web_path = self.file_path # TODO: use a better way to handle the file - self.temp_file = tempfile.NamedTemporaryFile() + self.temp_file = tempfile.NamedTemporaryFile() # S701 self.temp_file.write(r.content) self.file_path = self.temp_file.name elif not os.path.isfile(self.file_path): From 82ebe88c38cdf587dfa2caa5975910b16fc5e9b0 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 12 Sep 2024 17:20:22 +0800 Subject: [PATCH 57/97] chore: noqa SIM115 --- api/core/rag/extractor/word_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/rag/extractor/word_extractor.py b/api/core/rag/extractor/word_extractor.py index 5d65efd538..7352ef378b 100644 --- a/api/core/rag/extractor/word_extractor.py +++ b/api/core/rag/extractor/word_extractor.py @@ -49,7 +49,7 @@ class WordExtractor(BaseExtractor): self.web_path = self.file_path # TODO: use a better way to handle the file - self.temp_file = tempfile.NamedTemporaryFile() # S701 + self.temp_file = tempfile.NamedTemporaryFile() # noqa: SIM115 self.temp_file.write(r.content) self.file_path = self.temp_file.name elif not os.path.isfile(self.file_path): From 514f83936826e0b5f65c14eef9883be82477851b Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 14:49:54 +0800 Subject: [PATCH 58/97] feat: update EmailOrPasswordMismatchError --- api/controllers/console/auth/error.py | 6 ++++++ api/controllers/console/auth/login.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 98b4e96beb..24387d1173 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -35,3 +35,9 @@ class EmailCodeError(BaseHTTPException): error_code = "email_code_error" description = "Email code is invalid or expired." code = 400 + + +class EmailOrPasswordMismatchError(BaseHTTPException): + error_code = "email_or_password_mismatch" + description = "The email or password is incorrect." + code = 400 diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 06478cb58c..d3b843cd56 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -10,9 +10,9 @@ from constants.languages import languages from controllers.console import api from controllers.console.auth.error import ( EmailCodeError, + EmailOrPasswordMismatchError, InvalidEmailError, InvalidTokenError, - PasswordMismatchError, ) from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegister from controllers.console.setup import setup_required @@ -41,7 +41,7 @@ class LoginApi(Resource): except services.errors.account.AccountLoginError: raise NotAllowedRegister() except services.errors.account.AccountPasswordError: - raise PasswordMismatchError() + raise EmailOrPasswordMismatchError() except services.errors.account.AccountNotFoundError: if not dify_config.ALLOW_REGISTER: raise NotAllowedRegister() From 18adee68820d59254e925b7b744dd294475250fa Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 14:56:36 +0800 Subject: [PATCH 59/97] chore: PLR6201 Use a set literal when testing for membership --- 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 5b5b6bb652..6973775fcd 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -306,7 +306,7 @@ class AccountService: if not account: return None - if account.status in [AccountStatus.BANNED.value, AccountStatus.CLOSED.value]: + if account.status in {AccountStatus.BANNED.value, AccountStatus.CLOSED.value}: raise Unauthorized("Account is banned or closed.") return account From 20dc5559f2623d7054e529d712e8b27e52b36ee5 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 16:32:59 +0800 Subject: [PATCH 60/97] feat: remove env example --- api/.env.example | 10 +--------- docker/.env.example | 8 -------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/api/.env.example b/api/.env.example index 2de37af1ca..f775c1c5d3 100644 --- a/api/.env.example +++ b/api/.env.example @@ -289,12 +289,4 @@ POSITION_TOOL_EXCLUDES= POSITION_PROVIDER_PINS= POSITION_PROVIDER_INCLUDES= -POSITION_PROVIDER_EXCLUDES= - -# Login -ENABLE_EMAIL_CODE_LOGIN=true -ENABLE_EMAIL_PASSWORD_LOGIN=true -ENABLE_SOCIAL_OAUTH_LOGIN=true -EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=0.0833 -ALLOW_REGISTER=true -ALLOW_CREATE_WORKSPACE=true \ No newline at end of file +POSITION_PROVIDER_EXCLUDES= \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example index d394b538ec..51b058fc4a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -769,11 +769,3 @@ POSITION_TOOL_EXCLUDES= POSITION_PROVIDER_PINS= POSITION_PROVIDER_INCLUDES= POSITION_PROVIDER_EXCLUDES= - -# LoginConfig -ENABLE_EMAIL_CODE_LOGIN=true -ENABLE_EMAIL_PASSWORD_LOGIN=true -ENABLE_SOCIAL_OAUTH_LOGIN=true -EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS=0.0833 -ALLOW_REGISTER=true -ALLOW_CREATE_WORKSPACE=true From 61e4b3c822f6a27e4a27086e5ae55f0d6c56ea3e Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 16:36:10 +0800 Subject: [PATCH 61/97] feat: update mismatch description --- api/controllers/console/auth/error.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 24387d1173..3c1daebf71 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -27,7 +27,7 @@ class InvalidTokenError(BaseHTTPException): class PasswordResetRateLimitExceededError(BaseHTTPException): error_code = "password_reset_rate_limit_exceeded" - description = "Password reset rate limit exceeded. Try again later." + description = "Too many password reset emails have been sent. Please try again in 5 minutes." code = 429 @@ -39,5 +39,5 @@ class EmailCodeError(BaseHTTPException): class EmailOrPasswordMismatchError(BaseHTTPException): error_code = "email_or_password_mismatch" - description = "The email or password is incorrect." - code = 400 + description = "The email or password is mismatched." + code = 400 \ No newline at end of file From bc025856b41cea3dd601a4438421c2d65f187395 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 16:40:51 +0800 Subject: [PATCH 62/97] feat: update message --- api/controllers/console/auth/oauth.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index b15d28892a..0ad88d9b8e 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -84,16 +84,16 @@ class OAuthCallback(Resource): if invitation: invitation_email = invitation.get("email", None) if invitation_email != user_info.email: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=InvalidToken") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Invalid invitation token.") return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}") try: account = _generate_account(provider, user_info) except AccountNotFoundError: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=AccountNotFound") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Account not found.") except WorkSpaceNotFoundError: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found.") except WorkSpaceNotAllowedCreateError: return redirect( f"{dify_config.CONSOLE_WEB_URL}/signin" @@ -112,7 +112,7 @@ class OAuthCallback(Resource): try: TenantService.create_owner_tenant_if_not_exist(account) except Unauthorized: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=WorkspaceNotFound") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Worspace not found.") except WorkSpaceNotAllowedCreateError: return redirect( f"{dify_config.CONSOLE_WEB_URL}/signin" From 3e83be941c346722eeab63d027d405ad6fb86913 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 16:48:24 +0800 Subject: [PATCH 63/97] feat: add fulfill_login_params_from_env --- api/configs/feature/__init__.py | 4 ++-- api/services/feature_service.py | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 86de62dddd..7dbb80584a 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -611,7 +611,7 @@ class PositionConfig(BaseSettings): class LoginConfig(BaseSettings): ENABLE_EMAIL_CODE_LOGIN: bool = Field( description="whether to enable email code login", - default=True, + default=False, ) ENABLE_EMAIL_PASSWORD_LOGIN: bool = Field( description="whether to enable email password login", @@ -619,7 +619,7 @@ class LoginConfig(BaseSettings): ) ENABLE_SOCIAL_OAUTH_LOGIN: bool = Field( description="whether to enable github/google oauth login", - default=True, + default=False, ) EMAIL_CODE_LOGIN_TOKEN_EXPIRY_HOURS: PositiveFloat = Field( description="expiry time in hours for email code login token", diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 6a7eb8c34a..3b707d4bee 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -42,11 +42,11 @@ class SystemFeatureModel(BaseModel): sso_enforced_for_web: bool = False sso_enforced_for_web_protocol: str = "" enable_web_sso_switch_component: bool = False - enable_email_code_login: bool = dify_config.ENABLE_EMAIL_CODE_LOGIN - enable_email_password_login: bool = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN - enable_social_oauth_login: bool = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN - is_allow_register: bool = dify_config.ALLOW_REGISTER - is_allow_create_workspace: bool = dify_config.ALLOW_CREATE_WORKSPACE + enable_email_code_login: bool = False + enable_email_password_login: bool = True + enable_social_oauth_login: bool = False + is_allow_register: bool = True + is_allow_create_workspace: bool = True class FeatureService: @@ -65,12 +65,22 @@ class FeatureService: def get_system_features(cls) -> SystemFeatureModel: system_features = SystemFeatureModel() + cls.__fulfill_login_params_from_env(system_features) + if dify_config.ENTERPRISE_ENABLED: system_features.enable_web_sso_switch_component = True cls._fulfill_params_from_enterprise(system_features) return system_features + @classmethod + def __fulfill_login_params_from_env(cls, features: FeatureModel): + features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN + features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN + features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN + features.is_allow_register = dify_config.ALLOW_REGISTER + features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE + @classmethod def _fulfill_params_from_env(cls, features: FeatureModel): features.can_replace_logo = dify_config.CAN_REPLACE_LOGO From b529ba9c4514860cdfb7e068638df920542c1244 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 16:58:26 +0800 Subject: [PATCH 64/97] chore: format --- api/controllers/console/auth/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 3c1daebf71..d3e7a14506 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -40,4 +40,4 @@ class EmailCodeError(BaseHTTPException): class EmailOrPasswordMismatchError(BaseHTTPException): error_code = "email_or_password_mismatch" description = "The email or password is mismatched." - code = 400 \ No newline at end of file + code = 400 From 75fb4ca74d865668e0b67a25780ba04e5a8e8f80 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Sat, 14 Sep 2024 18:38:27 +0800 Subject: [PATCH 65/97] fix: spell --- api/controllers/console/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 0ad88d9b8e..f01fd57d6a 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -112,7 +112,7 @@ class OAuthCallback(Resource): try: TenantService.create_owner_tenant_if_not_exist(account) except Unauthorized: - return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Worspace not found.") + return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found.") except WorkSpaceNotAllowedCreateError: return redirect( f"{dify_config.CONSOLE_WEB_URL}/signin" From 346cae9b3c95cdf8e30219beb2d9e5194d468d0c Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 23 Sep 2024 10:17:51 +0800 Subject: [PATCH 66/97] feat: add email code login rate limiter --- api/services/account_service.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/services/account_service.py b/api/services/account_service.py index 6973775fcd..d9f67764df 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -45,6 +45,9 @@ from tasks.mail_reset_password_task import send_reset_password_mail_task class AccountService: reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=5, time_window=60 * 60) + email_code_login_rate_limiter = RateLimiter( + prefix="email_code_login_rate_limit", max_attempts=5, time_window=60 * 5 + ) @staticmethod def load_user(user_id: str) -> None | Account: @@ -280,6 +283,9 @@ class AccountService: @classmethod def send_email_code_login_email(cls, account: Optional[Account] = None, email: Optional[str] = None): + if cls.email_code_login_rate_limiter.is_rate_limited(email): + raise RateLimitExceededError(f"Rate limit exceeded for email: {email}. Please try again later.") + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) token = TokenManager.generate_token( account=account, email=email, token_type="email_code_login", additional_data={"code": code} @@ -289,7 +295,7 @@ class AccountService: to=account.email if account else email, code=code, ) - + cls.email_code_login_rate_limiter.increment_rate_limit(email) return token @classmethod From 799ff30d2820363c0e9056b45a2a8127c0ebed04 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 23 Sep 2024 10:45:30 +0800 Subject: [PATCH 67/97] feat: add email password limit --- api/controllers/console/auth/login.py | 9 +++++++-- api/services/account_service.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index d3b843cd56..9b19231f60 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -36,11 +36,16 @@ class LoginApi(Resource): parser.add_argument("remember_me", type=bool, required=False, default=False, location="json") args = parser.parse_args() + is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) + if is_login_error_rate_limit: + raise EmailOrPasswordMismatchError() + try: account = AccountService.authenticate(args["email"], args["password"]) except services.errors.account.AccountLoginError: raise NotAllowedRegister() except services.errors.account.AccountPasswordError: + AccountService.add_login_error_rate_limit(args["email"]) raise EmailOrPasswordMismatchError() except services.errors.account.AccountNotFoundError: if not dify_config.ALLOW_REGISTER: @@ -57,7 +62,7 @@ class LoginApi(Resource): } token = AccountService.login(account, ip_address=get_remote_ip(request)) - + AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token} @@ -154,7 +159,7 @@ class EmailCodeLoginApi(Resource): "?message=Workspace not found, please contact system admin to invite you to join in a workspace." ) token = AccountService.login(account, ip_address=get_remote_ip(request)) - + AccountService.reset_login_error_rate_limit(args["email"]) return {"result": "success", "data": token} diff --git a/api/services/account_service.py b/api/services/account_service.py index d9f67764df..7bd51454cb 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -48,6 +48,7 @@ class AccountService: email_code_login_rate_limiter = RateLimiter( prefix="email_code_login_rate_limit", max_attempts=5, time_window=60 * 5 ) + LOGIN_MAX_ERROR_LIMITS = 5 @staticmethod def load_user(user_id: str) -> None | Account: @@ -317,6 +318,32 @@ class AccountService: return account + @staticmethod + def add_login_error_rate_limit(email: str) -> None: + key = f"login_error_rate_limit:{email}" + count = redis_client.get(key) + if count is None: + count = 0 + count = int(count) + 1 + redis_client.setex(key, 60 * 60 * 24, count) + + @staticmethod + def is_login_error_rate_limit(email: str) -> bool: + key = f"login_error_rate_limit:{email}" + count = redis_client.get(key) + if count is None: + return False + + count = int(count) + if count > AccountService.LOGIN_MAX_ERROR_LIMITS: + return True + return False + + @staticmethod + def reset_login_error_rate_limit(email: str): + key = f"login_error_rate_limit:{email}" + redis_client.delete(key) + def _get_login_cache_key(*, account_id: str, token: str): return f"account_login:{account_id}:{token}" From 9223b0e6ae82deaeebd541600708931dc3bc5d22 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 23 Sep 2024 11:00:31 +0800 Subject: [PATCH 68/97] chore: style lint --- 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 7bd51454cb..688d45395a 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -333,12 +333,12 @@ class AccountService: count = redis_client.get(key) if count is None: return False - + count = int(count) if count > AccountService.LOGIN_MAX_ERROR_LIMITS: return True return False - + @staticmethod def reset_login_error_rate_limit(email: str): key = f"login_error_rate_limit:{email}" From d58726c8b30a5811123f14d38ec8d51918ae0def Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 23 Sep 2024 11:20:35 +0800 Subject: [PATCH 69/97] feat: add login limit error --- api/controllers/console/auth/error.py | 8 ++++++++ api/controllers/console/auth/login.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index d3e7a14506..4c102bda58 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -41,3 +41,11 @@ class EmailOrPasswordMismatchError(BaseHTTPException): error_code = "email_or_password_mismatch" description = "The email or password is mismatched." code = 400 + + +class EmailPasswordLoginLimitError(BaseHTTPException): + error_code = "email_code_login_limit" + description = ( + "Too many incorrect password attempts. Please verify your identity with the email code to complete login." + ) + code = 429 diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 9b19231f60..f574e7f998 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -11,6 +11,7 @@ from controllers.console import api from controllers.console.auth.error import ( EmailCodeError, EmailOrPasswordMismatchError, + EmailPasswordLoginLimitError, InvalidEmailError, InvalidTokenError, ) @@ -38,7 +39,7 @@ class LoginApi(Resource): is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) if is_login_error_rate_limit: - raise EmailOrPasswordMismatchError() + raise EmailPasswordLoginLimitError() try: account = AccountService.authenticate(args["email"], args["password"]) From 4494eea44d0c80d8b409cce320ae2f1106d6b5b6 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Mon, 23 Sep 2024 16:26:00 +0800 Subject: [PATCH 70/97] feat: update email_code_login_rate_limiter --- 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 688d45395a..59be3f7199 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -46,7 +46,7 @@ from tasks.mail_reset_password_task import send_reset_password_mail_task class AccountService: reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=5, time_window=60 * 60) email_code_login_rate_limiter = RateLimiter( - prefix="email_code_login_rate_limit", max_attempts=5, time_window=60 * 5 + prefix="email_code_login_rate_limit", max_attempts=1, time_window=60 * 1 ) LOGIN_MAX_ERROR_LIMITS = 5 From 34c97d686d151cd49f787df799b307002c4bce95 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 11:28:10 +0800 Subject: [PATCH 71/97] chore: change password type --- api/controllers/console/auth/login.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index f574e7f998..53278f44bd 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -19,7 +19,6 @@ from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegis from controllers.console.setup import setup_required from events.tenant_event import tenant_was_created from libs.helper import email, get_remote_ip -from libs.password import valid_password from models.account import Account from services.account_service import AccountService, TenantService from services.errors.workspace import WorkSpaceNotAllowedCreateError @@ -33,7 +32,7 @@ class LoginApi(Resource): """Authenticate user and login.""" parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") - parser.add_argument("password", type=valid_password, required=True, location="json") + parser.add_argument("password", type=str, required=True, location="json") parser.add_argument("remember_me", type=bool, required=False, default=False, location="json") args = parser.parse_args() From 0cbef254cd299e24a4c5c2a4af065b84b2167e8c Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 11:34:28 +0800 Subject: [PATCH 72/97] chore: change email template style --- api/templates/reset_password_mail_template_zh-CN.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/templates/reset_password_mail_template_zh-CN.html b/api/templates/reset_password_mail_template_zh-CN.html index 190fb091e8..9c81780a4c 100644 --- a/api/templates/reset_password_mail_template_zh-CN.html +++ b/api/templates/reset_password_mail_template_zh-CN.html @@ -63,7 +63,7 @@ Dify Logo -

重置您的Dify 账户密码

+

重置您的 Dify 账户密码

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

{{code}} From 5066233cd4f0e78db64d90731096de2bab41c3d1 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 14:30:06 +0800 Subject: [PATCH 73/97] feat: email api add password param --- api/controllers/console/auth/forgot_password.py | 9 +++++++-- api/controllers/console/auth/login.py | 14 ++++++++++---- api/services/account_service.py | 12 ++++++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 1a88d7d5d7..7b63b36e7d 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -31,18 +31,23 @@ class ForgotPasswordSendEmailApi(Resource): def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") + parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() account = Account.query.filter_by(email=args["email"]).first() token = None if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_reset_password_email(email=args["email"]) + token = AccountService.send_reset_password_email( + email=args["email"], language=args["language"] or "en-US" + ) else: raise NotAllowedRegister() elif account: try: - token = AccountService.send_reset_password_email(account=account, email=args["email"]) + token = AccountService.send_reset_password_email( + account=account, email=args["email"], language=args["language"] or "en-US" + ) except RateLimitExceededError: logging.warning(f"Rate limit exceeded for email: {args['email']}") raise PasswordResetRateLimitExceededError() diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 53278f44bd..a419561d42 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -81,16 +81,19 @@ class ResetPasswordSendEmailApi(Resource): def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") + parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() account = AccountService.get_user_through_email(args["email"]) if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_reset_password_email(email=args["email"]) + token = AccountService.send_reset_password_email( + email=args["email"], language=args["language"] or "en-US" + ) else: raise NotAllowedRegister() else: - token = AccountService.send_reset_password_email(account=account) + token = AccountService.send_reset_password_email(account=account, language=args["language"]) return {"result": "success", "data": token} @@ -100,16 +103,19 @@ class EmailCodeLoginSendEmailApi(Resource): def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") + parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() account = AccountService.get_user_through_email(args["email"]) if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_email_code_login_email(email=args["email"]) + token = AccountService.send_email_code_login_email( + email=args["email"], language=args["language"] or "en-US" + ) else: raise NotAllowedRegister() else: - token = AccountService.send_email_code_login_email(account=account) + token = AccountService.send_email_code_login_email(account=account, language=args["language"]) return {"result": "success", "data": token} diff --git a/api/services/account_service.py b/api/services/account_service.py index 59be3f7199..53e983d2e5 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -255,9 +255,11 @@ class AccountService: return AccountService.load_user(account_id) @classmethod - def send_reset_password_email(cls, account: Optional[Account] = None, email: Optional[str] = None): + def send_reset_password_email( + cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" + ): account_email = account.email if account else email - account_language = account.interface_language if account else languages[0] + account_language = account.interface_language if account else language if cls.reset_password_rate_limiter.is_rate_limited(account_email): raise RateLimitExceededError(f"Rate limit exceeded for email: {account_email}. Please try again later.") @@ -283,7 +285,9 @@ class AccountService: return TokenManager.get_token_data(token, "reset_password") @classmethod - def send_email_code_login_email(cls, account: Optional[Account] = None, email: Optional[str] = None): + def send_email_code_login_email( + cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" + ): if cls.email_code_login_rate_limiter.is_rate_limited(email): raise RateLimitExceededError(f"Rate limit exceeded for email: {email}. Please try again later.") @@ -292,7 +296,7 @@ class AccountService: account=account, email=email, token_type="email_code_login", additional_data={"code": code} ) send_email_code_login_mail_task.delay( - language=account.interface_language if account else languages[0], + language=account.interface_language if account else language, to=account.email if account else email, code=code, ) From 9e4ee2beb10c6dadcb6e6fd6c192ac8c7955a7a2 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 14:59:13 +0800 Subject: [PATCH 74/97] feat: add EmailPasswordLoginLimitError --- 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 53e983d2e5..cf4d65a27d 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -12,6 +12,7 @@ from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import language_timezone_mapping, languages +from controllers.console.auth.error import EmailPasswordLoginLimitError from events.tenant_event import tenant_was_created from extensions.ext_redis import redis_client from libs.helper import RateLimiter, TokenManager @@ -289,7 +290,7 @@ class AccountService: cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" ): if cls.email_code_login_rate_limiter.is_rate_limited(email): - raise RateLimitExceededError(f"Rate limit exceeded for email: {email}. Please try again later.") + raise EmailPasswordLoginLimitError() code = "".join([str(random.randint(0, 9)) for _ in range(6)]) token = TokenManager.generate_token( From b249f2b9f955f4feb0414543cd1bee4aa874aa23 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 15:49:37 +0800 Subject: [PATCH 75/97] feat: update raise error --- api/controllers/console/auth/error.py | 6 ++++++ api/controllers/console/auth/forgot_password.py | 12 ++++-------- api/services/account_service.py | 12 +++++++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 4c102bda58..4658d2c1e9 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -49,3 +49,9 @@ class EmailPasswordLoginLimitError(BaseHTTPException): "Too many incorrect password attempts. Please verify your identity with the email code to complete login." ) code = 429 + + +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 diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 7b63b36e7d..04b911d5e5 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -43,14 +43,10 @@ class ForgotPasswordSendEmailApi(Resource): ) else: raise NotAllowedRegister() - elif account: - try: - token = AccountService.send_reset_password_email( - account=account, email=args["email"], language=args["language"] or "en-US" - ) - except RateLimitExceededError: - logging.warning(f"Rate limit exceeded for email: {args['email']}") - raise PasswordResetRateLimitExceededError() + else: + token = AccountService.send_reset_password_email( + account=account, email=args["email"], language=args["language"] or "en-US" + ) return {"result": "success", "data": token} diff --git a/api/services/account_service.py b/api/services/account_service.py index cf4d65a27d..65d9959818 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -12,7 +12,6 @@ from werkzeug.exceptions import Unauthorized from configs import dify_config from constants.languages import language_timezone_mapping, languages -from controllers.console.auth.error import EmailPasswordLoginLimitError from events.tenant_event import tenant_was_created from extensions.ext_redis import redis_client from libs.helper import RateLimiter, TokenManager @@ -34,7 +33,6 @@ from services.errors.account import ( LinkAccountIntegrateError, MemberNotInTenantError, NoPermissionError, - RateLimitExceededError, RoleAlreadyAssignedError, TenantNotFoundError, ) @@ -45,7 +43,7 @@ from tasks.mail_reset_password_task import send_reset_password_mail_task class AccountService: - reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=5, time_window=60 * 60) + reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=1, time_window=60 * 1) email_code_login_rate_limiter = RateLimiter( prefix="email_code_login_rate_limit", max_attempts=1, time_window=60 * 1 ) @@ -263,7 +261,9 @@ class AccountService: account_language = account.interface_language if account else language if cls.reset_password_rate_limiter.is_rate_limited(account_email): - raise RateLimitExceededError(f"Rate limit exceeded for email: {account_email}. Please try again later.") + from controllers.console.auth.error import PasswordResetRateLimitExceededError + + raise PasswordResetRateLimitExceededError() code = "".join([str(random.randint(0, 9)) for _ in range(6)]) token = TokenManager.generate_token( @@ -290,7 +290,9 @@ class AccountService: cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" ): if cls.email_code_login_rate_limiter.is_rate_limited(email): - raise EmailPasswordLoginLimitError() + from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError + + raise EmailCodeLoginRateLimitExceededError() code = "".join([str(random.randint(0, 9)) for _ in range(6)]) token = TokenManager.generate_token( From 8059706e1884daf105669f28b707737933f41eae Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 15:50:19 +0800 Subject: [PATCH 76/97] chore: style lint --- api/controllers/console/auth/forgot_password.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 04b911d5e5..04999b60eb 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -1,5 +1,4 @@ import base64 -import logging import secrets from flask import request @@ -13,7 +12,6 @@ from controllers.console.auth.error import ( InvalidEmailError, InvalidTokenError, PasswordMismatchError, - PasswordResetRateLimitExceededError, ) from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegister from controllers.console.setup import setup_required @@ -23,7 +21,6 @@ from libs.helper import email, get_remote_ip from libs.password import hash_password, valid_password from models.account import Account from services.account_service import AccountService, TenantService -from services.errors.account import RateLimitExceededError class ForgotPasswordSendEmailApi(Resource): From 456e7a4abd058811de84c90abb9dc6bb1a75190a Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Thu, 26 Sep 2024 16:15:23 +0800 Subject: [PATCH 77/97] feat: change email language --- .../console/auth/forgot_password.py | 13 ++++++----- api/controllers/console/auth/login.py | 22 ++++++++++++------- api/services/account_service.py | 5 ++--- 3 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 04999b60eb..005f38b8e5 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -31,19 +31,20 @@ class ForgotPasswordSendEmailApi(Resource): parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() + if args["language"] is not None and args["language"] == "zh-Hans": + language = "zh-Hans" + else: + language = "en-US" + account = Account.query.filter_by(email=args["email"]).first() token = None if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_reset_password_email( - email=args["email"], language=args["language"] or "en-US" - ) + token = AccountService.send_reset_password_email(email=args["email"], language=language) else: raise NotAllowedRegister() else: - token = AccountService.send_reset_password_email( - account=account, email=args["email"], language=args["language"] or "en-US" - ) + token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language) return {"result": "success", "data": token} diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index a419561d42..601449af2e 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -84,16 +84,19 @@ class ResetPasswordSendEmailApi(Resource): parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() + if args["language"] is not None and args["language"] == "zh-Hans": + language = "zh-Hans" + else: + language = "en-US" + account = AccountService.get_user_through_email(args["email"]) if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_reset_password_email( - email=args["email"], language=args["language"] or "en-US" - ) + token = AccountService.send_reset_password_email(email=args["email"], language=language) else: raise NotAllowedRegister() else: - token = AccountService.send_reset_password_email(account=account, language=args["language"]) + token = AccountService.send_reset_password_email(account=account, language=language) return {"result": "success", "data": token} @@ -106,16 +109,19 @@ class EmailCodeLoginSendEmailApi(Resource): parser.add_argument("language", type=str, required=False, location="json") args = parser.parse_args() + if args["language"] is not None and args["language"] == "zh-Hans": + language = "zh-Hans" + else: + language = "en-US" + account = AccountService.get_user_through_email(args["email"]) if account is None: if dify_config.ALLOW_REGISTER: - token = AccountService.send_email_code_login_email( - email=args["email"], language=args["language"] or "en-US" - ) + token = AccountService.send_email_code_login_email(email=args["email"], language=language) else: raise NotAllowedRegister() else: - token = AccountService.send_email_code_login_email(account=account, language=args["language"]) + token = AccountService.send_email_code_login_email(account=account, language=language) return {"result": "success", "data": token} diff --git a/api/services/account_service.py b/api/services/account_service.py index 65d9959818..59aded2292 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -258,7 +258,6 @@ class AccountService: cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US" ): account_email = account.email if account else email - account_language = account.interface_language if account else language if cls.reset_password_rate_limiter.is_rate_limited(account_email): from controllers.console.auth.error import PasswordResetRateLimitExceededError @@ -270,7 +269,7 @@ class AccountService: account=account, email=email, token_type="reset_password", additional_data={"code": code} ) send_reset_password_mail_task.delay( - language=account_language, + language=language, to=account_email, code=code, ) @@ -299,7 +298,7 @@ class AccountService: account=account, email=email, token_type="email_code_login", additional_data={"code": code} ) send_email_code_login_mail_task.delay( - language=account.interface_language if account else language, + language=language, to=account.email if account else email, code=code, ) From 3801378f5e1cb69c1cfa33396dbd08cb4288313c Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Fri, 27 Sep 2024 10:52:01 +0800 Subject: [PATCH 78/97] feat: change valid_password error resp --- api/controllers/console/auth/login.py | 3 ++- api/libs/password.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 601449af2e..8925555fcc 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -19,6 +19,7 @@ from controllers.console.error import NotAllowedCreateWorkspace, NotAllowedRegis from controllers.console.setup import setup_required from events.tenant_event import tenant_was_created from libs.helper import email, get_remote_ip +from libs.password import valid_password from models.account import Account from services.account_service import AccountService, TenantService from services.errors.workspace import WorkSpaceNotAllowedCreateError @@ -33,7 +34,7 @@ class LoginApi(Resource): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("password", type=str, required=True, location="json") - parser.add_argument("remember_me", type=bool, required=False, default=False, location="json") + parser.add_argument("remember_me", type=valid_password, required=False, default=False, location="json") args = parser.parse_args() is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) diff --git a/api/libs/password.py b/api/libs/password.py index cfcc0db22d..cdf55c57e5 100644 --- a/api/libs/password.py +++ b/api/libs/password.py @@ -13,7 +13,7 @@ def valid_password(password): if re.match(pattern, password) is not None: return password - raise ValueError("Not a valid password.") + raise ValueError("Password must contain letters and numbers, and the length must be greater than 8.") def hash_password(password_str, salt_byte): From 133beea4914de7b40a395947d42bb14b78521b44 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Fri, 27 Sep 2024 11:56:37 +0800 Subject: [PATCH 79/97] fix: change type error --- api/controllers/console/auth/login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py index 8925555fcc..029a99bb3f 100644 --- a/api/controllers/console/auth/login.py +++ b/api/controllers/console/auth/login.py @@ -33,8 +33,8 @@ class LoginApi(Resource): """Authenticate user and login.""" parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") - parser.add_argument("password", type=str, required=True, location="json") - parser.add_argument("remember_me", type=valid_password, required=False, default=False, location="json") + parser.add_argument("password", type=valid_password, required=True, location="json") + parser.add_argument("remember_me", type=bool, required=False, default=False, location="json") args = parser.parse_args() is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) From 62f14088b5f5ec6f2d7da15a672358be97291eff Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Fri, 27 Sep 2024 12:48:15 +0800 Subject: [PATCH 80/97] feat: change error resp --- api/controllers/console/auth/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 4658d2c1e9..aa638be135 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -46,7 +46,7 @@ class EmailOrPasswordMismatchError(BaseHTTPException): class EmailPasswordLoginLimitError(BaseHTTPException): error_code = "email_code_login_limit" description = ( - "Too many incorrect password attempts. Please verify your identity with the email code to complete login." + "The account was locked for 24 hours because the password was entered too many times." ) code = 429 From d3d0d6451ab78b5f3615fb88263f067e73d98da3 Mon Sep 17 00:00:00 2001 From: Joe <1264204425@qq.com> Date: Fri, 27 Sep 2024 12:49:58 +0800 Subject: [PATCH 81/97] chore: change invite member template --- api/templates/invite_member_mail_template_en-US.html | 2 +- api/templates/invite_member_mail_template_zh-CN.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/templates/invite_member_mail_template_en-US.html b/api/templates/invite_member_mail_template_en-US.html index 80f7d42c20..e8bf7f5a52 100644 --- a/api/templates/invite_member_mail_template_en-US.html +++ b/api/templates/invite_member_mail_template_en-US.html @@ -59,7 +59,7 @@

Dear {{ to }},

{{ inviter_name }} is pleased to invite you to join our workspace on Dify, a platform specifically designed for LLM application development. On Dify, you can explore, create, and collaborate to build and operate AI applications.

-

You can now log in to Dify using the GitHub or Google account associated with this email.

+

Click the button below to log in to Dify and join the workspace.

Login Here