mirror of
https://github.com/langgenius/dify.git
synced 2026-01-14 06:07:33 +08:00
Feat/email register refactor (#25369)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
bb1514be2d
commit
c2fcd2895b
@ -530,6 +530,7 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
|
|||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,12 @@ class SecurityConfig(BaseSettings):
|
|||||||
description="Duration in minutes for which a password reset token remains valid",
|
description="Duration in minutes for which a password reset token remains valid",
|
||||||
default=5,
|
default=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
||||||
|
description="Duration in minutes for which a email register token remains valid",
|
||||||
|
default=5,
|
||||||
|
)
|
||||||
|
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
||||||
description="Duration in minutes for which a change email token remains valid",
|
description="Duration in minutes for which a change email token remains valid",
|
||||||
default=5,
|
default=5,
|
||||||
@ -639,6 +645,11 @@ class AuthConfig(BaseSettings):
|
|||||||
default=86400,
|
default=86400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
EMAIL_REGISTER_LOCKOUT_DURATION: PositiveInt = Field(
|
||||||
|
description="Time (in seconds) a user must wait before retrying email register after exceeding the rate limit.",
|
||||||
|
default=86400,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModerationConfig(BaseSettings):
|
class ModerationConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -93,6 +93,7 @@ from .auth import (
|
|||||||
activate, # pyright: ignore[reportUnusedImport]
|
activate, # pyright: ignore[reportUnusedImport]
|
||||||
data_source_bearer_auth, # pyright: ignore[reportUnusedImport]
|
data_source_bearer_auth, # pyright: ignore[reportUnusedImport]
|
||||||
data_source_oauth, # pyright: ignore[reportUnusedImport]
|
data_source_oauth, # pyright: ignore[reportUnusedImport]
|
||||||
|
email_register, # pyright: ignore[reportUnusedImport]
|
||||||
forgot_password, # pyright: ignore[reportUnusedImport]
|
forgot_password, # pyright: ignore[reportUnusedImport]
|
||||||
login, # pyright: ignore[reportUnusedImport]
|
login, # pyright: ignore[reportUnusedImport]
|
||||||
oauth, # pyright: ignore[reportUnusedImport]
|
oauth, # pyright: ignore[reportUnusedImport]
|
||||||
|
|||||||
155
api/controllers/console/auth/email_register.py
Normal file
155
api/controllers/console/auth/email_register.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
from flask import request
|
||||||
|
from flask_restx import Resource, reqparse
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from constants.languages import languages
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.auth.error import (
|
||||||
|
EmailAlreadyInUseError,
|
||||||
|
EmailCodeError,
|
||||||
|
EmailRegisterLimitError,
|
||||||
|
InvalidEmailError,
|
||||||
|
InvalidTokenError,
|
||||||
|
PasswordMismatchError,
|
||||||
|
)
|
||||||
|
from controllers.console.error import AccountInFreezeError, EmailSendIpLimitError
|
||||||
|
from controllers.console.wraps import email_password_login_enabled, email_register_enabled, setup_required
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from libs.helper import email, extract_remote_ip
|
||||||
|
from libs.password import valid_password
|
||||||
|
from models.account import Account
|
||||||
|
from services.account_service import AccountService
|
||||||
|
from services.billing_service import BillingService
|
||||||
|
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterSendEmailApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
|
@email_register_enabled
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
|
parser.add_argument("language", type=str, required=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ip_address = extract_remote_ip(request)
|
||||||
|
if AccountService.is_email_send_ip_limit(ip_address):
|
||||||
|
raise EmailSendIpLimitError()
|
||||||
|
language = "en-US"
|
||||||
|
if args["language"] in languages:
|
||||||
|
language = args["language"]
|
||||||
|
|
||||||
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
||||||
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
||||||
|
token = None
|
||||||
|
token = AccountService.send_email_register_email(email=args["email"], account=account, language=language)
|
||||||
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterCheckApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
|
@email_register_enabled
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("code", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
user_email = args["email"]
|
||||||
|
|
||||||
|
is_email_register_error_rate_limit = AccountService.is_email_register_error_rate_limit(args["email"])
|
||||||
|
if is_email_register_error_rate_limit:
|
||||||
|
raise EmailRegisterLimitError()
|
||||||
|
|
||||||
|
token_data = AccountService.get_email_register_data(args["token"])
|
||||||
|
if token_data is None:
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
if user_email != token_data.get("email"):
|
||||||
|
raise InvalidEmailError()
|
||||||
|
|
||||||
|
if args["code"] != token_data.get("code"):
|
||||||
|
AccountService.add_email_register_error_rate_limit(args["email"])
|
||||||
|
raise EmailCodeError()
|
||||||
|
|
||||||
|
# Verified, revoke the first token
|
||||||
|
AccountService.revoke_email_register_token(args["token"])
|
||||||
|
|
||||||
|
# Refresh token data by generating a new token
|
||||||
|
_, new_token = AccountService.generate_email_register_token(
|
||||||
|
user_email, code=args["code"], additional_data={"phase": "register"}
|
||||||
|
)
|
||||||
|
|
||||||
|
AccountService.reset_email_register_error_rate_limit(args["email"])
|
||||||
|
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterResetApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
|
@email_register_enabled
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
|
parser.add_argument("new_password", type=valid_password, required=True, nullable=False, location="json")
|
||||||
|
parser.add_argument("password_confirm", type=valid_password, required=True, nullable=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validate passwords match
|
||||||
|
if args["new_password"] != args["password_confirm"]:
|
||||||
|
raise PasswordMismatchError()
|
||||||
|
|
||||||
|
# Validate token and get register data
|
||||||
|
register_data = AccountService.get_email_register_data(args["token"])
|
||||||
|
if not register_data:
|
||||||
|
raise InvalidTokenError()
|
||||||
|
# Must use token in reset phase
|
||||||
|
if register_data.get("phase", "") != "register":
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
# Revoke token to prevent reuse
|
||||||
|
AccountService.revoke_email_register_token(args["token"])
|
||||||
|
|
||||||
|
email = register_data.get("email", "")
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if account:
|
||||||
|
raise EmailAlreadyInUseError()
|
||||||
|
else:
|
||||||
|
account = self._create_new_account(email, args["password_confirm"])
|
||||||
|
if not account:
|
||||||
|
raise AccountNotFoundError()
|
||||||
|
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
|
||||||
|
AccountService.reset_login_error_rate_limit(email)
|
||||||
|
|
||||||
|
return {"result": "success", "data": token_pair.model_dump()}
|
||||||
|
|
||||||
|
def _create_new_account(self, email, password) -> Account | None:
|
||||||
|
# Create new account if allowed
|
||||||
|
account = None
|
||||||
|
try:
|
||||||
|
account = AccountService.create_account_and_tenant(
|
||||||
|
email=email,
|
||||||
|
name=email,
|
||||||
|
password=password,
|
||||||
|
interface_language=languages[0],
|
||||||
|
)
|
||||||
|
except AccountRegisterError:
|
||||||
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(EmailRegisterSendEmailApi, "/email-register/send-email")
|
||||||
|
api.add_resource(EmailRegisterCheckApi, "/email-register/validity")
|
||||||
|
api.add_resource(EmailRegisterResetApi, "/email-register")
|
||||||
@ -27,21 +27,43 @@ class InvalidTokenError(BaseHTTPException):
|
|||||||
|
|
||||||
class PasswordResetRateLimitExceededError(BaseHTTPException):
|
class PasswordResetRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "password_reset_rate_limit_exceeded"
|
error_code = "password_reset_rate_limit_exceeded"
|
||||||
description = "Too many password reset emails have been sent. Please try again in 1 minute."
|
description = "Too many password reset emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterRateLimitExceededError(BaseHTTPException):
|
||||||
|
error_code = "email_register_rate_limit_exceeded"
|
||||||
|
description = "Too many email register emails have been sent. Please try again in {minutes} minutes."
|
||||||
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailChangeRateLimitExceededError(BaseHTTPException):
|
class EmailChangeRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "email_change_rate_limit_exceeded"
|
error_code = "email_change_rate_limit_exceeded"
|
||||||
description = "Too many email change emails have been sent. Please try again in 1 minute."
|
description = "Too many email change emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class OwnerTransferRateLimitExceededError(BaseHTTPException):
|
class OwnerTransferRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "owner_transfer_rate_limit_exceeded"
|
error_code = "owner_transfer_rate_limit_exceeded"
|
||||||
description = "Too many owner transfer emails have been sent. Please try again in 1 minute."
|
description = "Too many owner transfer emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailCodeError(BaseHTTPException):
|
class EmailCodeError(BaseHTTPException):
|
||||||
error_code = "email_code_error"
|
error_code = "email_code_error"
|
||||||
@ -69,15 +91,23 @@ class EmailPasswordLoginLimitError(BaseHTTPException):
|
|||||||
|
|
||||||
class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
|
class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "email_code_login_rate_limit_exceeded"
|
error_code = "email_code_login_rate_limit_exceeded"
|
||||||
description = "Too many login emails have been sent. Please try again in 5 minutes."
|
description = "Too many login emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 5):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
||||||
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
description = "Too many account deletion emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 5):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailPasswordResetLimitError(BaseHTTPException):
|
class EmailPasswordResetLimitError(BaseHTTPException):
|
||||||
error_code = "email_password_reset_limit"
|
error_code = "email_password_reset_limit"
|
||||||
@ -85,6 +115,12 @@ class EmailPasswordResetLimitError(BaseHTTPException):
|
|||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterLimitError(BaseHTTPException):
|
||||||
|
error_code = "email_register_limit"
|
||||||
|
description = "Too many failed email register attempts. Please try again in 24 hours."
|
||||||
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
class EmailChangeLimitError(BaseHTTPException):
|
class EmailChangeLimitError(BaseHTTPException):
|
||||||
error_code = "email_change_limit"
|
error_code = "email_change_limit"
|
||||||
description = "Too many failed email change attempts. Please try again in 24 hours."
|
description = "Too many failed email change attempts. Please try again in 24 hours."
|
||||||
|
|||||||
@ -6,7 +6,6 @@ from flask_restx import Resource, fields, reqparse
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from constants.languages import languages
|
|
||||||
from controllers.console import api, console_ns
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.auth.error import (
|
from controllers.console.auth.error import (
|
||||||
EmailCodeError,
|
EmailCodeError,
|
||||||
@ -15,7 +14,7 @@ from controllers.console.auth.error import (
|
|||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
PasswordMismatchError,
|
PasswordMismatchError,
|
||||||
)
|
)
|
||||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
|
||||||
from controllers.console.wraps import email_password_login_enabled, setup_required
|
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -23,8 +22,6 @@ from libs.helper import email, extract_remote_ip
|
|||||||
from libs.password import hash_password, valid_password
|
from libs.password import hash_password, valid_password
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import AccountService, TenantService
|
from services.account_service import AccountService, TenantService
|
||||||
from services.errors.account import AccountRegisterError
|
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
@ -73,15 +70,13 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
|
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
||||||
token = None
|
|
||||||
if account is None:
|
token = AccountService.send_reset_password_email(
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
account=account,
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
email=args["email"],
|
||||||
return {"result": "fail", "data": token, "code": "account_not_found"}
|
language=language,
|
||||||
else:
|
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||||
raise AccountNotFound()
|
)
|
||||||
else:
|
|
||||||
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
|
|
||||||
|
|
||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
@ -207,7 +202,7 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
if account:
|
if account:
|
||||||
self._update_existing_account(account, password_hashed, salt, session)
|
self._update_existing_account(account, password_hashed, salt, session)
|
||||||
else:
|
else:
|
||||||
self._create_new_account(email, args["password_confirm"])
|
raise AccountNotFound()
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
@ -227,18 +222,7 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
account.current_tenant = tenant
|
account.current_tenant = tenant
|
||||||
tenant_was_created.send(tenant)
|
tenant_was_created.send(tenant)
|
||||||
|
|
||||||
def _create_new_account(self, email, password):
|
|
||||||
# Create new account if allowed
|
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
|
||||||
try:
|
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
|
||||||
AccountService.create_account_and_tenant(
|
api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets")
|
||||||
email=email,
|
|
||||||
name=email,
|
|
||||||
password=password,
|
|
||||||
interface_language=languages[0],
|
|
||||||
)
|
|
||||||
except WorkSpaceNotAllowedCreateError:
|
|
||||||
pass
|
|
||||||
except WorkspacesLimitExceededError:
|
|
||||||
pass
|
|
||||||
except AccountRegisterError:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
|
|||||||
@ -26,7 +26,6 @@ from controllers.console.error import (
|
|||||||
from controllers.console.wraps import email_password_login_enabled, setup_required
|
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from libs.helper import email, extract_remote_ip
|
from libs.helper import email, extract_remote_ip
|
||||||
from libs.password import valid_password
|
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import AccountService, RegisterService, TenantService
|
from services.account_service import AccountService, RegisterService, TenantService
|
||||||
from services.billing_service import BillingService
|
from services.billing_service import BillingService
|
||||||
@ -44,10 +43,9 @@ class LoginApi(Resource):
|
|||||||
"""Authenticate user and login."""
|
"""Authenticate user and login."""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=email, required=True, location="json")
|
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")
|
parser.add_argument("remember_me", type=bool, required=False, default=False, location="json")
|
||||||
parser.add_argument("invite_token", type=str, required=False, default=None, location="json")
|
parser.add_argument("invite_token", type=str, required=False, default=None, location="json")
|
||||||
parser.add_argument("language", type=str, required=False, default="en-US", location="json")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
||||||
@ -61,11 +59,6 @@ class LoginApi(Resource):
|
|||||||
if invitation:
|
if invitation:
|
||||||
invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation)
|
invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation)
|
||||||
|
|
||||||
if args["language"] is not None and args["language"] == "zh-Hans":
|
|
||||||
language = "zh-Hans"
|
|
||||||
else:
|
|
||||||
language = "en-US"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if invitation:
|
if invitation:
|
||||||
data = invitation.get("data", {})
|
data = invitation.get("data", {})
|
||||||
@ -80,12 +73,6 @@ class LoginApi(Resource):
|
|||||||
except services.errors.account.AccountPasswordError:
|
except services.errors.account.AccountPasswordError:
|
||||||
AccountService.add_login_error_rate_limit(args["email"])
|
AccountService.add_login_error_rate_limit(args["email"])
|
||||||
raise AuthenticationFailedError()
|
raise AuthenticationFailedError()
|
||||||
except services.errors.account.AccountNotFoundError:
|
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
|
||||||
return {"result": "fail", "data": token, "code": "account_not_found"}
|
|
||||||
else:
|
|
||||||
raise AccountNotFound()
|
|
||||||
# SELF_HOSTED only have one workspace
|
# SELF_HOSTED only have one workspace
|
||||||
tenants = TenantService.get_join_tenants(account)
|
tenants = TenantService.get_join_tenants(account)
|
||||||
if len(tenants) == 0:
|
if len(tenants) == 0:
|
||||||
@ -133,13 +120,12 @@ class ResetPasswordSendEmailApi(Resource):
|
|||||||
except AccountRegisterError:
|
except AccountRegisterError:
|
||||||
raise AccountInFreezeError()
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
if account is None:
|
token = AccountService.send_reset_password_email(
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
email=args["email"],
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
account=account,
|
||||||
else:
|
language=language,
|
||||||
raise AccountNotFound()
|
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||||
else:
|
)
|
||||||
token = AccountService.send_reset_password_email(account=account, language=language)
|
|
||||||
|
|
||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
|
|||||||
from models import Account
|
from models import Account
|
||||||
from models.account import AccountStatus
|
from models.account import AccountStatus
|
||||||
from services.account_service import AccountService, RegisterService, TenantService
|
from services.account_service import AccountService, RegisterService, TenantService
|
||||||
|
from services.billing_service import BillingService
|
||||||
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
@ -183,7 +184,15 @@ def _generate_account(provider: str, user_info: OAuthUserInfo):
|
|||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
if not FeatureService.get_system_features().is_allow_register:
|
if not FeatureService.get_system_features().is_allow_register:
|
||||||
raise AccountNotFoundError()
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(user_info.email):
|
||||||
|
raise AccountRegisterError(
|
||||||
|
description=(
|
||||||
|
"This email account has been deleted within the past "
|
||||||
|
"30 days and is temporarily unavailable for new account registration"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise AccountRegisterError(description=("Invalid email or password"))
|
||||||
account_name = user_info.name or "Dify"
|
account_name = user_info.name or "Dify"
|
||||||
account = RegisterService.register(
|
account = RegisterService.register(
|
||||||
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider
|
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider
|
||||||
|
|||||||
@ -242,6 +242,19 @@ def email_password_login_enabled(view: Callable[P, R]):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def email_register_enabled(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
features = FeatureService.get_system_features()
|
||||||
|
if features.is_allow_register:
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
# otherwise, return 403
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def enable_change_email(view: Callable[P, R]):
|
def enable_change_email(view: Callable[P, R]):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
|
|||||||
@ -21,6 +21,7 @@ class EmailType(Enum):
|
|||||||
"""Enumeration of supported email types."""
|
"""Enumeration of supported email types."""
|
||||||
|
|
||||||
RESET_PASSWORD = "reset_password"
|
RESET_PASSWORD = "reset_password"
|
||||||
|
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST = "reset_password_when_account_not_exist"
|
||||||
INVITE_MEMBER = "invite_member"
|
INVITE_MEMBER = "invite_member"
|
||||||
EMAIL_CODE_LOGIN = "email_code_login"
|
EMAIL_CODE_LOGIN = "email_code_login"
|
||||||
CHANGE_EMAIL_OLD = "change_email_old"
|
CHANGE_EMAIL_OLD = "change_email_old"
|
||||||
@ -34,6 +35,9 @@ class EmailType(Enum):
|
|||||||
ENTERPRISE_CUSTOM = "enterprise_custom"
|
ENTERPRISE_CUSTOM = "enterprise_custom"
|
||||||
QUEUE_MONITOR_ALERT = "queue_monitor_alert"
|
QUEUE_MONITOR_ALERT = "queue_monitor_alert"
|
||||||
DOCUMENT_CLEAN_NOTIFY = "document_clean_notify"
|
DOCUMENT_CLEAN_NOTIFY = "document_clean_notify"
|
||||||
|
EMAIL_REGISTER = "email_register"
|
||||||
|
EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = "email_register_when_account_exist"
|
||||||
|
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = "reset_password_when_account_not_exist_no_register"
|
||||||
|
|
||||||
|
|
||||||
class EmailLanguage(Enum):
|
class EmailLanguage(Enum):
|
||||||
@ -441,6 +445,54 @@ def create_default_email_config() -> EmailI18nConfig:
|
|||||||
branded_template_path="clean_document_job_mail_template_zh-CN.html",
|
branded_template_path="clean_document_job_mail_template_zh-CN.html",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
EmailType.EMAIL_REGISTER: {
|
||||||
|
EmailLanguage.EN_US: EmailTemplate(
|
||||||
|
subject="Register Your {application_title} Account",
|
||||||
|
template_path="register_email_template_en-US.html",
|
||||||
|
branded_template_path="without-brand/register_email_template_en-US.html",
|
||||||
|
),
|
||||||
|
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||||
|
subject="注册您的 {application_title} 账户",
|
||||||
|
template_path="register_email_template_zh-CN.html",
|
||||||
|
branded_template_path="without-brand/register_email_template_zh-CN.html",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
EmailType.EMAIL_REGISTER_WHEN_ACCOUNT_EXIST: {
|
||||||
|
EmailLanguage.EN_US: EmailTemplate(
|
||||||
|
subject="Register Your {application_title} Account",
|
||||||
|
template_path="register_email_when_account_exist_template_en-US.html",
|
||||||
|
branded_template_path="without-brand/register_email_when_account_exist_template_en-US.html",
|
||||||
|
),
|
||||||
|
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||||
|
subject="注册您的 {application_title} 账户",
|
||||||
|
template_path="register_email_when_account_exist_template_zh-CN.html",
|
||||||
|
branded_template_path="without-brand/register_email_when_account_exist_template_zh-CN.html",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST: {
|
||||||
|
EmailLanguage.EN_US: EmailTemplate(
|
||||||
|
subject="Reset Your {application_title} Password",
|
||||||
|
template_path="reset_password_mail_when_account_not_exist_template_en-US.html",
|
||||||
|
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_template_en-US.html",
|
||||||
|
),
|
||||||
|
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||||
|
subject="重置您的 {application_title} 密码",
|
||||||
|
template_path="reset_password_mail_when_account_not_exist_template_zh-CN.html",
|
||||||
|
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_template_zh-CN.html",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER: {
|
||||||
|
EmailLanguage.EN_US: EmailTemplate(
|
||||||
|
subject="Reset Your {application_title} Password",
|
||||||
|
template_path="reset_password_mail_when_account_not_exist_no_register_template_en-US.html",
|
||||||
|
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_no_register_template_en-US.html",
|
||||||
|
),
|
||||||
|
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||||
|
subject="重置您的 {application_title} 密码",
|
||||||
|
template_path="reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html",
|
||||||
|
branded_template_path="without-brand/reset_password_mail_when_account_not_exist_no_register_template_zh-CN.html",
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return EmailI18nConfig(templates=templates)
|
return EmailI18nConfig(templates=templates)
|
||||||
|
|||||||
@ -37,7 +37,6 @@ from services.billing_service import BillingService
|
|||||||
from services.errors.account import (
|
from services.errors.account import (
|
||||||
AccountAlreadyInTenantError,
|
AccountAlreadyInTenantError,
|
||||||
AccountLoginError,
|
AccountLoginError,
|
||||||
AccountNotFoundError,
|
|
||||||
AccountNotLinkTenantError,
|
AccountNotLinkTenantError,
|
||||||
AccountPasswordError,
|
AccountPasswordError,
|
||||||
AccountRegisterError,
|
AccountRegisterError,
|
||||||
@ -65,7 +64,11 @@ from tasks.mail_owner_transfer_task import (
|
|||||||
send_old_owner_transfer_notify_email_task,
|
send_old_owner_transfer_notify_email_task,
|
||||||
send_owner_transfer_confirm_task,
|
send_owner_transfer_confirm_task,
|
||||||
)
|
)
|
||||||
from tasks.mail_reset_password_task import send_reset_password_mail_task
|
from tasks.mail_register_task import send_email_register_mail_task, send_email_register_mail_task_when_account_exist
|
||||||
|
from tasks.mail_reset_password_task import (
|
||||||
|
send_reset_password_mail_task,
|
||||||
|
send_reset_password_mail_task_when_account_not_exist,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -82,8 +85,9 @@ REFRESH_TOKEN_EXPIRY = timedelta(days=dify_config.REFRESH_TOKEN_EXPIRE_DAYS)
|
|||||||
|
|
||||||
class AccountService:
|
class AccountService:
|
||||||
reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=1, time_window=60 * 1)
|
reset_password_rate_limiter = RateLimiter(prefix="reset_password_rate_limit", max_attempts=1, time_window=60 * 1)
|
||||||
|
email_register_rate_limiter = RateLimiter(prefix="email_register_rate_limit", max_attempts=1, time_window=60 * 1)
|
||||||
email_code_login_rate_limiter = RateLimiter(
|
email_code_login_rate_limiter = RateLimiter(
|
||||||
prefix="email_code_login_rate_limit", max_attempts=1, time_window=60 * 1
|
prefix="email_code_login_rate_limit", max_attempts=3, time_window=300 * 1
|
||||||
)
|
)
|
||||||
email_code_account_deletion_rate_limiter = RateLimiter(
|
email_code_account_deletion_rate_limiter = RateLimiter(
|
||||||
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
||||||
@ -95,6 +99,7 @@ class AccountService:
|
|||||||
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
||||||
CHANGE_EMAIL_MAX_ERROR_LIMITS = 5
|
CHANGE_EMAIL_MAX_ERROR_LIMITS = 5
|
||||||
OWNER_TRANSFER_MAX_ERROR_LIMITS = 5
|
OWNER_TRANSFER_MAX_ERROR_LIMITS = 5
|
||||||
|
EMAIL_REGISTER_MAX_ERROR_LIMITS = 5
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_refresh_token_key(refresh_token: str) -> str:
|
def _get_refresh_token_key(refresh_token: str) -> str:
|
||||||
@ -171,7 +176,7 @@ class AccountService:
|
|||||||
|
|
||||||
account = db.session.query(Account).filter_by(email=email).first()
|
account = db.session.query(Account).filter_by(email=email).first()
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountNotFoundError()
|
raise AccountPasswordError("Invalid email or password.")
|
||||||
|
|
||||||
if account.status == AccountStatus.BANNED.value:
|
if account.status == AccountStatus.BANNED.value:
|
||||||
raise AccountLoginError("Account is banned.")
|
raise AccountLoginError("Account is banned.")
|
||||||
@ -296,7 +301,9 @@ class AccountService:
|
|||||||
if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email):
|
if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email):
|
||||||
from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError
|
from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError
|
||||||
|
|
||||||
raise EmailCodeAccountDeletionRateLimitExceededError()
|
raise EmailCodeAccountDeletionRateLimitExceededError(
|
||||||
|
int(cls.email_code_account_deletion_rate_limiter.time_window / 60)
|
||||||
|
)
|
||||||
|
|
||||||
send_account_deletion_verification_code.delay(to=email, code=code)
|
send_account_deletion_verification_code.delay(to=email, code=code)
|
||||||
|
|
||||||
@ -435,6 +442,7 @@ class AccountService:
|
|||||||
account: Optional[Account] = None,
|
account: Optional[Account] = None,
|
||||||
email: Optional[str] = None,
|
email: Optional[str] = None,
|
||||||
language: str = "en-US",
|
language: str = "en-US",
|
||||||
|
is_allow_register: bool = False,
|
||||||
):
|
):
|
||||||
account_email = account.email if account else email
|
account_email = account.email if account else email
|
||||||
if account_email is None:
|
if account_email is None:
|
||||||
@ -443,18 +451,59 @@ class AccountService:
|
|||||||
if cls.reset_password_rate_limiter.is_rate_limited(account_email):
|
if cls.reset_password_rate_limiter.is_rate_limited(account_email):
|
||||||
from controllers.console.auth.error import PasswordResetRateLimitExceededError
|
from controllers.console.auth.error import PasswordResetRateLimitExceededError
|
||||||
|
|
||||||
raise PasswordResetRateLimitExceededError()
|
raise PasswordResetRateLimitExceededError(int(cls.reset_password_rate_limiter.time_window / 60))
|
||||||
|
|
||||||
code, token = cls.generate_reset_password_token(account_email, account)
|
code, token = cls.generate_reset_password_token(account_email, account)
|
||||||
|
|
||||||
send_reset_password_mail_task.delay(
|
if account:
|
||||||
language=language,
|
send_reset_password_mail_task.delay(
|
||||||
to=account_email,
|
language=language,
|
||||||
code=code,
|
to=account_email,
|
||||||
)
|
code=code,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
send_reset_password_mail_task_when_account_not_exist.delay(
|
||||||
|
language=language,
|
||||||
|
to=account_email,
|
||||||
|
is_allow_register=is_allow_register,
|
||||||
|
)
|
||||||
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_email_register_email(
|
||||||
|
cls,
|
||||||
|
account: Optional[Account] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
language: str = "en-US",
|
||||||
|
):
|
||||||
|
account_email = account.email if account else email
|
||||||
|
if account_email is None:
|
||||||
|
raise ValueError("Email must be provided.")
|
||||||
|
|
||||||
|
if cls.email_register_rate_limiter.is_rate_limited(account_email):
|
||||||
|
from controllers.console.auth.error import EmailRegisterRateLimitExceededError
|
||||||
|
|
||||||
|
raise EmailRegisterRateLimitExceededError(int(cls.email_register_rate_limiter.time_window / 60))
|
||||||
|
|
||||||
|
code, token = cls.generate_email_register_token(account_email)
|
||||||
|
|
||||||
|
if account:
|
||||||
|
send_email_register_mail_task_when_account_exist.delay(
|
||||||
|
language=language,
|
||||||
|
to=account_email,
|
||||||
|
account_name=account.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
send_email_register_mail_task.delay(
|
||||||
|
language=language,
|
||||||
|
to=account_email,
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
cls.email_register_rate_limiter.increment_rate_limit(account_email)
|
||||||
|
return token
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_change_email_email(
|
def send_change_email_email(
|
||||||
cls,
|
cls,
|
||||||
@ -473,7 +522,7 @@ class AccountService:
|
|||||||
if cls.change_email_rate_limiter.is_rate_limited(account_email):
|
if cls.change_email_rate_limiter.is_rate_limited(account_email):
|
||||||
from controllers.console.auth.error import EmailChangeRateLimitExceededError
|
from controllers.console.auth.error import EmailChangeRateLimitExceededError
|
||||||
|
|
||||||
raise EmailChangeRateLimitExceededError()
|
raise EmailChangeRateLimitExceededError(int(cls.change_email_rate_limiter.time_window / 60))
|
||||||
|
|
||||||
code, token = cls.generate_change_email_token(account_email, account, old_email=old_email)
|
code, token = cls.generate_change_email_token(account_email, account, old_email=old_email)
|
||||||
|
|
||||||
@ -517,7 +566,7 @@ class AccountService:
|
|||||||
if cls.owner_transfer_rate_limiter.is_rate_limited(account_email):
|
if cls.owner_transfer_rate_limiter.is_rate_limited(account_email):
|
||||||
from controllers.console.auth.error import OwnerTransferRateLimitExceededError
|
from controllers.console.auth.error import OwnerTransferRateLimitExceededError
|
||||||
|
|
||||||
raise OwnerTransferRateLimitExceededError()
|
raise OwnerTransferRateLimitExceededError(int(cls.owner_transfer_rate_limiter.time_window / 60))
|
||||||
|
|
||||||
code, token = cls.generate_owner_transfer_token(account_email, account)
|
code, token = cls.generate_owner_transfer_token(account_email, account)
|
||||||
workspace_name = workspace_name or ""
|
workspace_name = workspace_name or ""
|
||||||
@ -587,6 +636,19 @@ class AccountService:
|
|||||||
)
|
)
|
||||||
return code, token
|
return code, token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_email_register_token(
|
||||||
|
cls,
|
||||||
|
email: str,
|
||||||
|
code: Optional[str] = None,
|
||||||
|
additional_data: dict[str, Any] = {},
|
||||||
|
):
|
||||||
|
if not code:
|
||||||
|
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
|
||||||
|
additional_data["code"] = code
|
||||||
|
token = TokenManager.generate_token(email=email, token_type="email_register", additional_data=additional_data)
|
||||||
|
return code, token
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_change_email_token(
|
def generate_change_email_token(
|
||||||
cls,
|
cls,
|
||||||
@ -625,6 +687,10 @@ class AccountService:
|
|||||||
def revoke_reset_password_token(cls, token: str):
|
def revoke_reset_password_token(cls, token: str):
|
||||||
TokenManager.revoke_token(token, "reset_password")
|
TokenManager.revoke_token(token, "reset_password")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def revoke_email_register_token(cls, token: str):
|
||||||
|
TokenManager.revoke_token(token, "email_register")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def revoke_change_email_token(cls, token: str):
|
def revoke_change_email_token(cls, token: str):
|
||||||
TokenManager.revoke_token(token, "change_email")
|
TokenManager.revoke_token(token, "change_email")
|
||||||
@ -637,6 +703,10 @@ class AccountService:
|
|||||||
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
|
def get_reset_password_data(cls, token: str) -> Optional[dict[str, Any]]:
|
||||||
return TokenManager.get_token_data(token, "reset_password")
|
return TokenManager.get_token_data(token, "reset_password")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_email_register_data(cls, token: str) -> Optional[dict[str, Any]]:
|
||||||
|
return TokenManager.get_token_data(token, "email_register")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]:
|
def get_change_email_data(cls, token: str) -> Optional[dict[str, Any]]:
|
||||||
return TokenManager.get_token_data(token, "change_email")
|
return TokenManager.get_token_data(token, "change_email")
|
||||||
@ -658,7 +728,7 @@ class AccountService:
|
|||||||
if cls.email_code_login_rate_limiter.is_rate_limited(email):
|
if cls.email_code_login_rate_limiter.is_rate_limited(email):
|
||||||
from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError
|
from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError
|
||||||
|
|
||||||
raise EmailCodeLoginRateLimitExceededError()
|
raise EmailCodeLoginRateLimitExceededError(int(cls.email_code_login_rate_limiter.time_window / 60))
|
||||||
|
|
||||||
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
|
code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)])
|
||||||
token = TokenManager.generate_token(
|
token = TokenManager.generate_token(
|
||||||
@ -744,6 +814,16 @@ class AccountService:
|
|||||||
count = int(count) + 1
|
count = int(count) + 1
|
||||||
redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count)
|
redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@redis_fallback(default_return=None)
|
||||||
|
def add_email_register_error_rate_limit(email: str) -> None:
|
||||||
|
key = f"email_register_error_rate_limit:{email}"
|
||||||
|
count = redis_client.get(key)
|
||||||
|
if count is None:
|
||||||
|
count = 0
|
||||||
|
count = int(count) + 1
|
||||||
|
redis_client.setex(key, dify_config.EMAIL_REGISTER_LOCKOUT_DURATION, count)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@redis_fallback(default_return=False)
|
@redis_fallback(default_return=False)
|
||||||
def is_forgot_password_error_rate_limit(email: str) -> bool:
|
def is_forgot_password_error_rate_limit(email: str) -> bool:
|
||||||
@ -763,6 +843,24 @@ class AccountService:
|
|||||||
key = f"forgot_password_error_rate_limit:{email}"
|
key = f"forgot_password_error_rate_limit:{email}"
|
||||||
redis_client.delete(key)
|
redis_client.delete(key)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@redis_fallback(default_return=False)
|
||||||
|
def is_email_register_error_rate_limit(email: str) -> bool:
|
||||||
|
key = f"email_register_error_rate_limit:{email}"
|
||||||
|
count = redis_client.get(key)
|
||||||
|
if count is None:
|
||||||
|
return False
|
||||||
|
count = int(count)
|
||||||
|
if count > AccountService.EMAIL_REGISTER_MAX_ERROR_LIMITS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@redis_fallback(default_return=None)
|
||||||
|
def reset_email_register_error_rate_limit(email: str):
|
||||||
|
key = f"email_register_error_rate_limit:{email}"
|
||||||
|
redis_client.delete(key)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@redis_fallback(default_return=None)
|
@redis_fallback(default_return=None)
|
||||||
def add_change_email_error_rate_limit(email: str):
|
def add_change_email_error_rate_limit(email: str):
|
||||||
|
|||||||
87
api/tasks/mail_register_task.py
Normal file
87
api/tasks/mail_register_task.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from extensions.ext_mail import mail
|
||||||
|
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="mail")
|
||||||
|
def send_email_register_mail_task(language: str, to: str, code: str) -> None:
|
||||||
|
"""
|
||||||
|
Send email register email with internationalization support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
language: Language code for email localization
|
||||||
|
to: Recipient email address
|
||||||
|
code: Email register code
|
||||||
|
"""
|
||||||
|
if not mail.is_inited():
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(click.style(f"Start email register mail to {to}", fg="green"))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
email_service = get_email_i18n_service()
|
||||||
|
email_service.send_email(
|
||||||
|
email_type=EmailType.EMAIL_REGISTER,
|
||||||
|
language_code=language,
|
||||||
|
to=to,
|
||||||
|
template_context={
|
||||||
|
"to": to,
|
||||||
|
"code": code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logger.info(
|
||||||
|
click.style(f"Send email register mail to {to} succeeded: latency: {end_at - start_at}", fg="green")
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Send email register mail to %s failed", to)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="mail")
|
||||||
|
def send_email_register_mail_task_when_account_exist(language: str, to: str, account_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Send email register email with internationalization support when account exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
language: Language code for email localization
|
||||||
|
to: Recipient email address
|
||||||
|
"""
|
||||||
|
if not mail.is_inited():
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(click.style(f"Start email register mail to {to}", fg="green"))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
login_url = f"{dify_config.CONSOLE_WEB_URL}/signin"
|
||||||
|
reset_password_url = f"{dify_config.CONSOLE_WEB_URL}/reset-password"
|
||||||
|
|
||||||
|
email_service = get_email_i18n_service()
|
||||||
|
email_service.send_email(
|
||||||
|
email_type=EmailType.EMAIL_REGISTER_WHEN_ACCOUNT_EXIST,
|
||||||
|
language_code=language,
|
||||||
|
to=to,
|
||||||
|
template_context={
|
||||||
|
"to": to,
|
||||||
|
"login_url": login_url,
|
||||||
|
"reset_password_url": reset_password_url,
|
||||||
|
"account_name": account_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logger.info(
|
||||||
|
click.style(f"Send email register mail to {to} succeeded: latency: {end_at - start_at}", fg="green")
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Send email register mail to %s failed", to)
|
||||||
@ -4,6 +4,7 @@ import time
|
|||||||
import click
|
import click
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from extensions.ext_mail import mail
|
from extensions.ext_mail import mail
|
||||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||||
|
|
||||||
@ -44,3 +45,47 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Send password reset mail to %s failed", to)
|
logger.exception("Send password reset mail to %s failed", to)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="mail")
|
||||||
|
def send_reset_password_mail_task_when_account_not_exist(language: str, to: str, is_allow_register: bool) -> None:
|
||||||
|
"""
|
||||||
|
Send reset password email with internationalization support when account not exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
language: Language code for email localization
|
||||||
|
to: Recipient email address
|
||||||
|
"""
|
||||||
|
if not mail.is_inited():
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(click.style(f"Start password reset mail to {to}", fg="green"))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if is_allow_register:
|
||||||
|
sign_up_url = f"{dify_config.CONSOLE_WEB_URL}/signup"
|
||||||
|
email_service = get_email_i18n_service()
|
||||||
|
email_service.send_email(
|
||||||
|
email_type=EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST,
|
||||||
|
language_code=language,
|
||||||
|
to=to,
|
||||||
|
template_context={
|
||||||
|
"to": to,
|
||||||
|
"sign_up_url": sign_up_url,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
email_service = get_email_i18n_service()
|
||||||
|
email_service.send_email(
|
||||||
|
email_type=EmailType.RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER,
|
||||||
|
language_code=language,
|
||||||
|
to=to,
|
||||||
|
)
|
||||||
|
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logger.info(
|
||||||
|
click.style(f"Send password reset mail to {to} succeeded: latency: {end_at - start_at}", fg="green")
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Send password reset mail to %s failed", to)
|
||||||
|
|||||||
87
api/templates/register_email_template_en-US.html
Normal file
87
api/templates/register_email_template_en-US.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">Dify Sign-up Code</p>
|
||||||
|
<p class="description">Your sign-up code for Dify
|
||||||
|
|
||||||
|
Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">If you didn't request this code, don't worry. You can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
87
api/templates/register_email_template_zh-CN.html
Normal file
87
api/templates/register_email_template_zh-CN.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">Dify 注册验证码</p>
|
||||||
|
<p class="description">您的 Dify 注册验证码
|
||||||
|
|
||||||
|
复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">如果您没有请求,请不要担心。您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">It looks like you’re signing up with an existing account</p>
|
||||||
|
<p class="description">Hi, {{account_name}}</p>
|
||||||
|
<p class="description">
|
||||||
|
We noticed you tried to sign up, but this email is already registered with an existing account.
|
||||||
|
|
||||||
|
Please log in here: </p>
|
||||||
|
<a href="{{ login_url }}" class="button">Log In</a>
|
||||||
|
<p class="description">
|
||||||
|
If you forgot your password, you can reset it here: <a href="{{ reset_password_url }}"
|
||||||
|
class="reset-btn">Reset Password</a>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
If you didn’t request this action, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">您似乎正在使用现有账户注册</p>
|
||||||
|
<p class="description">您好,{{account_name}}</p>
|
||||||
|
<p class="description">
|
||||||
|
我们注意到您尝试注册,但此电子邮件已注册。
|
||||||
|
|
||||||
|
请在此登录: </p>
|
||||||
|
<a href="{{ login_url }}" class="button">登录</a>
|
||||||
|
<p class="description">
|
||||||
|
如果您忘记了密码,可以在此重置: <a href="{{ reset_password_url }}" class="reset-btn">重置密码</a>
|
||||||
|
</p>
|
||||||
|
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">It looks like you’re resetting a password with an unregistered email</p>
|
||||||
|
<p class="description">Hi, </p>
|
||||||
|
<p class="description">
|
||||||
|
We noticed you tried to reset your password, but this email is not associated with any account.
|
||||||
|
</p>
|
||||||
|
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">看起来您正在使用未注册的电子邮件重置密码</p>
|
||||||
|
<p class="description">您好,</p>
|
||||||
|
<p class="description">
|
||||||
|
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。</p>
|
||||||
|
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,124 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">It looks like you’re resetting a password with an unregistered email</p>
|
||||||
|
<p class="description">Hi, </p>
|
||||||
|
<p class="description">
|
||||||
|
We noticed you tried to reset your password, but this email is not associated with any account.
|
||||||
|
|
||||||
|
Please sign up here: </p>
|
||||||
|
<a href="{{ sign_up_url }}" class="button">Sign Up</a>
|
||||||
|
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<!-- Optional: Add a logo or a header image here -->
|
||||||
|
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||||
|
</div>
|
||||||
|
<p class="title">看起来您正在使用未注册的电子邮件重置密码</p>
|
||||||
|
<p class="description">您好, </p>
|
||||||
|
<p class="description">
|
||||||
|
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
|
||||||
|
|
||||||
|
请在此注册: </p>
|
||||||
|
<p class="description">
|
||||||
|
<a href="{{ sign_up_url }}" class="button">注册</a>
|
||||||
|
</p>
|
||||||
|
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">{{application_title}} Sign-up Code</p>
|
||||||
|
<p class="description">Your sign-up code
|
||||||
|
|
||||||
|
Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">If you didn't request this code, don't worry. You can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">{{application_title}} 注册验证码</p>
|
||||||
|
<p class="description">您的 {{application_title}} 注册验证码
|
||||||
|
|
||||||
|
复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">如果您没有请求此验证码,请不要担心。您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">It looks like you’re signing up with an existing account</p>
|
||||||
|
<p class="description">Hi, {{account_name}}</p>
|
||||||
|
<p class="description">
|
||||||
|
We noticed you tried to sign up, but this email is already registered with an existing account.
|
||||||
|
|
||||||
|
Please log in here: </p>
|
||||||
|
<a href="{{ login_url }}" class="button">Log In</a>
|
||||||
|
<p class="description">
|
||||||
|
If you forgot your password, you can reset it here: <a href="{{ reset_password_url }}"
|
||||||
|
class="reset-btn">Reset Password</a>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
If you didn’t request this action, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">您似乎正在使用现有账户注册</p>
|
||||||
|
<p class="description">您好,{{account_name}}</p>
|
||||||
|
<p class="description">
|
||||||
|
我们注意到您尝试注册,但此电子邮件已注册。
|
||||||
|
|
||||||
|
请在此登录: </p>
|
||||||
|
<a href="{{ login_url }}" class="button">登录</a>
|
||||||
|
<p class="description">
|
||||||
|
如果您忘记了密码,可以在此重置: <a href="{{ reset_password_url }}" class="reset-btn">重置密码</a>
|
||||||
|
</p>
|
||||||
|
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">It looks like you’re resetting a password with an unregistered email</p>
|
||||||
|
<p class="description">Hi,</p>
|
||||||
|
<p class="description">
|
||||||
|
We noticed you tried to reset your password, but this email is not associated with any account.
|
||||||
|
</p>
|
||||||
|
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>s
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">看起来您正在使用未注册的电子邮件重置密码</p>
|
||||||
|
<p class="description">您好,</p>
|
||||||
|
<p class="description">
|
||||||
|
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
|
||||||
|
</p>
|
||||||
|
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">It looks like you’re resetting a password with an unregistered email</p>
|
||||||
|
<p class="description">Hi,</p>
|
||||||
|
<p class="description">
|
||||||
|
We noticed you tried to reset your password, but this email is not associated with any account.
|
||||||
|
|
||||||
|
Please sign up here: </p>
|
||||||
|
<a href="{{ sign_up_url }}" class="button">Sign Up</a>
|
||||||
|
<p class="description">If you didn’t request this action, you can safely ignore this email.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="tip">Please do not reply directly to this email, it is automatically sent by the system.</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
margin: 80px auto 0 auto;
|
||||||
|
padding: 36px 48px 52px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 3px 10px -2px rgba(9, 9, 11, 0.08), 0 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #676f83;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid rgba(16, 24, 40, 0.04);
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 -6px 12px -4px rgba(9, 9, 11, 0.08) inset, 0 0 1px 0 rgba(255, 255, 255, 0.16) inset, 0 0.5px 0 0 rgba(255, 255, 255, 0.08) inset, 0 2px 2px -1px rgba(0, 0, 0, 0.12), 0 1px 1px -1px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(9, 9, 11, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support {
|
||||||
|
color: #155AEF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #676F83;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">看起来您正在使用未注册的电子邮件重置密码</p>
|
||||||
|
<p class="description">您好, </p>
|
||||||
|
<p class="description">
|
||||||
|
我们注意到您尝试重置密码,但此电子邮件未与任何账户关联。
|
||||||
|
|
||||||
|
请在此注册: </p>
|
||||||
|
<a href="{{ sign_up_url }}" class="button">注册</a>
|
||||||
|
<p class="description">如果您没有请求此操作,您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
<div class="tip">请不要直接回复此电子邮件,它是由系统自动发送的。</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -203,6 +203,7 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
|
|||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@ from services.account_service import AccountService, RegisterService, TenantServ
|
|||||||
from services.errors.account import (
|
from services.errors.account import (
|
||||||
AccountAlreadyInTenantError,
|
AccountAlreadyInTenantError,
|
||||||
AccountLoginError,
|
AccountLoginError,
|
||||||
AccountNotFoundError,
|
|
||||||
AccountPasswordError,
|
AccountPasswordError,
|
||||||
AccountRegisterError,
|
AccountRegisterError,
|
||||||
CurrentPasswordIncorrectError,
|
CurrentPasswordIncorrectError,
|
||||||
@ -161,7 +160,7 @@ class TestAccountService:
|
|||||||
fake = Faker()
|
fake = Faker()
|
||||||
email = fake.email()
|
email = fake.email()
|
||||||
password = fake.password(length=12)
|
password = fake.password(length=12)
|
||||||
with pytest.raises(AccountNotFoundError):
|
with pytest.raises(AccountPasswordError):
|
||||||
AccountService.authenticate(email, password)
|
AccountService.authenticate(email, password)
|
||||||
|
|
||||||
def test_authenticate_banned_account(self, db_session_with_containers, mock_external_service_dependencies):
|
def test_authenticate_banned_account(self, db_session_with_containers, mock_external_service_dependencies):
|
||||||
|
|||||||
@ -9,7 +9,6 @@ from flask_restx import Api
|
|||||||
import services.errors.account
|
import services.errors.account
|
||||||
from controllers.console.auth.error import AuthenticationFailedError
|
from controllers.console.auth.error import AuthenticationFailedError
|
||||||
from controllers.console.auth.login import LoginApi
|
from controllers.console.auth.login import LoginApi
|
||||||
from controllers.console.error import AccountNotFound
|
|
||||||
|
|
||||||
|
|
||||||
class TestAuthenticationSecurity:
|
class TestAuthenticationSecurity:
|
||||||
@ -27,31 +26,33 @@ class TestAuthenticationSecurity:
|
|||||||
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
||||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||||
@patch("controllers.console.auth.login.AccountService.send_reset_password_email")
|
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
|
||||||
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
|
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
|
||||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
|
@patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
|
||||||
def test_login_invalid_email_with_registration_allowed(
|
def test_login_invalid_email_with_registration_allowed(
|
||||||
self, mock_get_invitation, mock_send_email, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
|
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
|
||||||
):
|
):
|
||||||
"""Test that invalid email sends reset password email when registration is allowed."""
|
"""Test that invalid email raises AuthenticationFailedError when account not found."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_is_rate_limit.return_value = False
|
mock_is_rate_limit.return_value = False
|
||||||
mock_get_invitation.return_value = None
|
mock_get_invitation.return_value = None
|
||||||
mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
|
mock_authenticate.side_effect = services.errors.account.AccountPasswordError("Invalid email or password.")
|
||||||
mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
|
mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
|
||||||
mock_features.return_value.is_allow_register = True
|
mock_features.return_value.is_allow_register = True
|
||||||
mock_send_email.return_value = "token123"
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with self.app.test_request_context(
|
with self.app.test_request_context(
|
||||||
"/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
|
"/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
|
||||||
):
|
):
|
||||||
login_api = LoginApi()
|
login_api = LoginApi()
|
||||||
result = login_api.post()
|
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result == {"result": "fail", "data": "token123", "code": "account_not_found"}
|
with pytest.raises(AuthenticationFailedError) as exc_info:
|
||||||
mock_send_email.assert_called_once_with(email="nonexistent@example.com", language="en-US")
|
login_api.post()
|
||||||
|
|
||||||
|
assert exc_info.value.error_code == "authentication_failed"
|
||||||
|
assert exc_info.value.description == "Invalid email or password."
|
||||||
|
mock_add_rate_limit.assert_called_once_with("nonexistent@example.com")
|
||||||
|
|
||||||
@patch("controllers.console.wraps.db")
|
@patch("controllers.console.wraps.db")
|
||||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||||
@ -87,16 +88,17 @@ class TestAuthenticationSecurity:
|
|||||||
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
||||||
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
@patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
|
||||||
@patch("controllers.console.auth.login.AccountService.authenticate")
|
@patch("controllers.console.auth.login.AccountService.authenticate")
|
||||||
|
@patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
|
||||||
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
|
@patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
|
||||||
@patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
|
@patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
|
||||||
def test_login_invalid_email_with_registration_disabled(
|
def test_login_invalid_email_with_registration_disabled(
|
||||||
self, mock_get_invitation, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
|
self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
|
||||||
):
|
):
|
||||||
"""Test that invalid email raises AccountNotFound when registration is disabled."""
|
"""Test that invalid email raises AuthenticationFailedError when account not found."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_is_rate_limit.return_value = False
|
mock_is_rate_limit.return_value = False
|
||||||
mock_get_invitation.return_value = None
|
mock_get_invitation.return_value = None
|
||||||
mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
|
mock_authenticate.side_effect = services.errors.account.AccountPasswordError("Invalid email or password.")
|
||||||
mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
|
mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
|
||||||
mock_features.return_value.is_allow_register = False
|
mock_features.return_value.is_allow_register = False
|
||||||
|
|
||||||
@ -107,10 +109,12 @@ class TestAuthenticationSecurity:
|
|||||||
login_api = LoginApi()
|
login_api = LoginApi()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
with pytest.raises(AccountNotFound) as exc_info:
|
with pytest.raises(AuthenticationFailedError) as exc_info:
|
||||||
login_api.post()
|
login_api.post()
|
||||||
|
|
||||||
assert exc_info.value.error_code == "account_not_found"
|
assert exc_info.value.error_code == "authentication_failed"
|
||||||
|
assert exc_info.value.description == "Invalid email or password."
|
||||||
|
mock_add_rate_limit.assert_called_once_with("nonexistent@example.com")
|
||||||
|
|
||||||
@patch("controllers.console.wraps.db")
|
@patch("controllers.console.wraps.db")
|
||||||
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
@patch("controllers.console.auth.login.FeatureService.get_system_features")
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from controllers.console.auth.oauth import (
|
|||||||
)
|
)
|
||||||
from libs.oauth import OAuthUserInfo
|
from libs.oauth import OAuthUserInfo
|
||||||
from models.account import AccountStatus
|
from models.account import AccountStatus
|
||||||
from services.errors.account import AccountNotFoundError
|
from services.errors.account import AccountRegisterError
|
||||||
|
|
||||||
|
|
||||||
class TestGetOAuthProviders:
|
class TestGetOAuthProviders:
|
||||||
@ -451,7 +451,7 @@ class TestAccountGeneration:
|
|||||||
|
|
||||||
with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}):
|
with app.test_request_context(headers={"Accept-Language": "en-US,en;q=0.9"}):
|
||||||
if not allow_register and not existing_account:
|
if not allow_register and not existing_account:
|
||||||
with pytest.raises(AccountNotFoundError):
|
with pytest.raises(AccountRegisterError):
|
||||||
_generate_account("github", user_info)
|
_generate_account("github", user_info)
|
||||||
else:
|
else:
|
||||||
result = _generate_account("github", user_info)
|
result = _generate_account("github", user_info)
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from services.account_service import AccountService, RegisterService, TenantServ
|
|||||||
from services.errors.account import (
|
from services.errors.account import (
|
||||||
AccountAlreadyInTenantError,
|
AccountAlreadyInTenantError,
|
||||||
AccountLoginError,
|
AccountLoginError,
|
||||||
AccountNotFoundError,
|
|
||||||
AccountPasswordError,
|
AccountPasswordError,
|
||||||
AccountRegisterError,
|
AccountRegisterError,
|
||||||
CurrentPasswordIncorrectError,
|
CurrentPasswordIncorrectError,
|
||||||
@ -195,7 +194,7 @@ class TestAccountService:
|
|||||||
|
|
||||||
# Execute test and verify exception
|
# Execute test and verify exception
|
||||||
self._assert_exception_raised(
|
self._assert_exception_raised(
|
||||||
AccountNotFoundError, AccountService.authenticate, "notfound@example.com", "password"
|
AccountPasswordError, AccountService.authenticate, "notfound@example.com", "password"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_authenticate_account_banned(self, mock_db_dependencies):
|
def test_authenticate_account_banned(self, mock_db_dependencies):
|
||||||
|
|||||||
@ -843,6 +843,7 @@ INVITE_EXPIRY_HOURS=72
|
|||||||
|
|
||||||
# Reset password token valid time (minutes),
|
# Reset password token valid time (minutes),
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
@ -372,6 +372,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
||||||
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES: ${EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: ${CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES:-5}
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: ${CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: ${OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES:-5}
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES: ${OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194}
|
CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user