feat: support email as admin login

pull/21891/head
ytqh 1 year ago
parent 6f3bd7c6a5
commit 3a82e83dad

@ -59,6 +59,11 @@ class SchoolConfig(BaseSettings):
default=None,
)
DEBUG_ADMIN_EMAIL: Optional[str] = Field(
description="Debug admin email for DEMO school-level features.",
default=None,
)
DEBUG_CODE_FOR_LOGIN: Optional[str] = Field(
description="Default code for login",
default=None,

@ -1,12 +1,11 @@
from typing import cast
import flask_login # type: ignore
from flask import request
from flask_restful import Resource, reqparse # type: ignore
from configs import dify_config
from controllers.admin import api
from controllers.service_api_with_auth.error import AccountInFreezeError
from flask import request
from flask_restful import Resource, reqparse # type: ignore
from libs.helper import extract_remote_ip
from models.account import Account
from services.account_service import AccountService
@ -15,12 +14,12 @@ from services.errors.account import AccountRegisterError
class SendVerificationCodeApi(Resource):
def post(self):
"""Send verification code to admin's phone number.
"""Send verification code to admin's phone number or email.
---
tags:
- admin/api/auth
summary: Send Verification Code
description: Sends a verification code to the provided admin phone number for authentication
description: Sends a verification code to the provided admin phone number or email for authentication
parameters:
- in: body
name: body
@ -28,12 +27,12 @@ class SendVerificationCodeApi(Resource):
schema:
type: object
required:
- phone
- login_id
properties:
phone:
login_id:
type: string
description: Admin's phone number
example: "13800138000"
description: Admin's phone number or email address
example: "admin@test.edu"
responses:
200:
description: Code sent successfully
@ -45,42 +44,60 @@ class SendVerificationCodeApi(Resource):
data:
type: string
400:
description: Invalid phone number format
description: Invalid input format or missing required fields
404:
description: Phone number not registered as admin
description: Phone number or email not registered as admin
"""
parser = reqparse.RequestParser()
parser.add_argument("phone", type=str, required=True, location="json")
parser.add_argument("login_id", type=str, required=True, location="json")
args = parser.parse_args()
phone = args["phone"]
login_id = args.get("login_id")
# Determine if login_id is an email or phone number
is_email = "@" in login_id
ip_address = extract_remote_ip(request)
if AccountService.is_phone_send_ip_limit(ip_address) and phone != dify_config.DEBUG_ADMIN_PHONE:
return {"result": "fail", "data": "Too many requests from this IP address"}, 429
if AccountService.is_login_attempt_ip_limit(ip_address) and login_id not in {
dify_config.DEBUG_ADMIN_EMAIL,
dify_config.DEBUG_ADMIN_PHONE,
}:
return {
"result": "fail",
"data": "Too many requests from this IP address",
}, 429
try:
# find account by phone number & chech role is end_admin
account = AccountService.get_admin_through_phone(phone)
# Use the unified method to check admin account
account = AccountService.get_admin_through_login_id(login_id)
except AccountRegisterError:
raise AccountInFreezeError()
if account is None:
return {"result": "fail", "data": "Phone number not registered as admin"}, 404
error_type = "Email" if is_email else "Phone number"
return {
"result": "fail",
"data": f"{error_type} not registered as admin",
}, 404
token = AccountService.send_phone_code_login(phone=phone)
# Send verification code
if is_email:
token = AccountService.send_email_code_login_email(email=login_id)
else:
token = AccountService.send_phone_code_login(phone=login_id)
return {"result": "success", "data": token}
class LoginApi(Resource):
def post(self):
"""Admin login with phone number and verification code.
"""Admin login with phone number/email and verification code.
---
tags:
- admin/api/auth
summary: Admin Login
description: Authenticates an admin using phone number and verification code
description: Authenticates an admin using phone number/email and verification code
parameters:
- in: body
name: body
@ -88,14 +105,14 @@ class LoginApi(Resource):
schema:
type: object
required:
- phone
- login_id
- code
- token
properties:
phone:
login_id:
type: string
description: Admin's phone number
example: "13800138000"
description: Admin's phone number or email address
example: "admin@test.edu"
code:
type: string
description: Verification code
@ -123,6 +140,8 @@ class LoginApi(Resource):
type: string
phone:
type: string
email:
type: string
name:
type: string
role:
@ -131,20 +150,41 @@ class LoginApi(Resource):
400:
description: Invalid or expired verification code
404:
description: Phone number not registered
description: Phone number or email not registered
"""
parser = reqparse.RequestParser()
parser.add_argument("phone", type=str, required=True, location="json")
parser.add_argument("login_id", type=str, required=True, location="json")
parser.add_argument("code", type=str, required=True, location="json")
parser.add_argument("token", type=str, required=True, location="json")
args = parser.parse_args()
# Verify the token and code
login_id = args.get("login_id")
# Determine if login_id is an email or phone number
is_email = "@" in login_id
# Handle verification based on identified type
if is_email:
# Email-based verification
token_data = AccountService.get_email_code_login_data(args["token"])
if token_data is None:
return {"result": "fail", "data": "Invalid or expired token"}, 400
if token_data.get("email") != login_id:
return {"result": "fail", "data": "Email does not match"}, 400
if token_data["code"] != args["code"]:
return {"result": "fail", "data": "Invalid verification code"}, 400
# Revoke the token after successful verification
AccountService.revoke_email_code_login_token(args["token"])
else:
# Phone-based verification
token_data = AccountService.get_phone_code_login_data(args["token"])
if token_data is None:
return {"result": "fail", "data": "Invalid or expired token"}, 400
if token_data["phone"] != args["phone"]:
if token_data["phone"] != login_id:
return {"result": "fail", "data": "Phone number does not match"}, 400
if token_data["code"] != args["code"]:
@ -154,16 +194,25 @@ class LoginApi(Resource):
AccountService.revoke_phone_code_login_token(args["token"])
try:
account = AccountService.get_admin_through_phone(args["phone"])
# Use the unified method to get admin account
account = AccountService.get_admin_through_login_id(login_id)
except AccountRegisterError:
raise AccountInFreezeError()
if account is None:
return {"result": "fail", "data": "Phone number not registered as admin"}, 404
error_type = "Email" if is_email else "Phone number"
return {
"result": "fail",
"data": f"{error_type} not registered as admin",
}, 404
# Reset login error rate limit
AccountService.reset_login_error_rate_limit(login_id)
# Generate token for the authenticated admin
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(args["phone"])
token_pair = AccountService.login(
account, ip_address=extract_remote_ip(request)
)
response_data = token_pair.model_dump()
@ -250,7 +299,7 @@ class RefreshTokenApi(Resource):
# Register the resources
api.add_resource(SendVerificationCodeApi, '/auth/send-code')
api.add_resource(LoginApi, '/auth/login')
api.add_resource(LogoutApi, '/auth/logout')
api.add_resource(RefreshTokenApi, '/auth/refresh-token')
api.add_resource(SendVerificationCodeApi, "/auth/send-code")
api.add_resource(LoginApi, "/auth/login")
api.add_resource(LogoutApi, "/auth/logout")
api.add_resource(RefreshTokenApi, "/auth/refresh-token")

@ -70,7 +70,9 @@ REFRESH_TOKEN_EXPIRY = timedelta(days=dify_config.REFRESH_TOKEN_EXPIRE_DAYS)
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_code_login_rate_limiter = RateLimiter(
prefix="email_code_login_rate_limit", max_attempts=1, time_window=60 * 1
)
@ -120,12 +122,16 @@ class AccountService:
if account.status == AccountStatus.BANNED.value:
raise Unauthorized("Account is banned.")
current_tenant = TenantAccountJoin.query.filter_by(account_id=account.id, current=True).first()
current_tenant = TenantAccountJoin.query.filter_by(
account_id=account.id, current=True
).first()
if current_tenant:
account.current_tenant_id = current_tenant.tenant_id
else:
available_ta = (
TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first()
TenantAccountJoin.query.filter_by(account_id=account.id)
.order_by(TenantAccountJoin.id.asc())
.first()
)
if not available_ta:
return None
@ -134,7 +140,9 @@ class AccountService:
available_ta.current = True
db.session.commit()
if datetime.now(UTC).replace(tzinfo=None) - account.last_active_at > timedelta(minutes=10):
if datetime.now(UTC).replace(tzinfo=None) - account.last_active_at > timedelta(
minutes=10
):
account.last_active_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
@ -142,7 +150,9 @@ class AccountService:
@staticmethod
def get_account_jwt_token(account: Account) -> str:
exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)
exp_dt = datetime.now(UTC) + timedelta(
minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES
)
exp = int(exp_dt.timestamp())
payload = {
"user_id": account.id,
@ -155,7 +165,9 @@ class AccountService:
return token
@staticmethod
def authenticate(email: str, password: str, invite_token: Optional[str] = None) -> Account:
def authenticate(
email: str, password: str, invite_token: Optional[str] = None
) -> Account:
"""authenticate account with email and password"""
account = db.session.query(Account).filter_by(email=email).first()
@ -174,7 +186,9 @@ class AccountService:
account.password = base64_password_hashed
account.password_salt = base64_salt
if account.password is None or not compare_password(password, account.password, account.password_salt):
if account.password is None or not compare_password(
password, account.password, account.password_salt
):
raise AccountPasswordError("Invalid email or password.")
if account.status == AccountStatus.PENDING.value:
@ -188,7 +202,9 @@ class AccountService:
@staticmethod
def update_account_password(account, password, new_password):
"""update account password"""
if account.password and not compare_password(password, account.password, account.password_salt):
if account.password and not compare_password(
password, account.password, account.password_salt
):
raise CurrentPasswordIncorrectError("Current password is incorrect.")
# may be raised
@ -305,7 +321,9 @@ class AccountService:
def send_account_deletion_verification_email(cls, account: Account, code: str):
email = account.email
if cls.email_code_account_deletion_rate_limiter.is_rate_limited(email):
from controllers.console.auth.error import EmailCodeAccountDeletionRateLimitExceededError
from controllers.console.auth.error import (
EmailCodeAccountDeletionRateLimitExceededError,
)
raise EmailCodeAccountDeletionRateLimitExceededError()
@ -334,9 +352,11 @@ class AccountService:
"""Link account integrate"""
try:
# Query whether there is an existing binding record for the same provider
account_integrate: Optional[AccountIntegrate] = AccountIntegrate.query.filter_by(
account_integrate: Optional[AccountIntegrate] = (
AccountIntegrate.query.filter_by(
account_id=account.id, provider=provider
).first()
)
if account_integrate:
# If it exists, update the record
@ -356,7 +376,9 @@ class AccountService:
db.session.commit()
logging.info(f"Account {account.id} linked {provider} account {open_id}.")
except Exception as e:
logging.exception(f"Failed to link {provider} account {open_id} to Account {account.id}")
logging.exception(
f"Failed to link {provider} account {open_id} to Account {account.id}"
)
raise LinkAccountIntegrateError("Failed to link account.") from e
@staticmethod
@ -403,14 +425,20 @@ class AccountService:
@staticmethod
def logout(*, account: Account) -> None:
refresh_token = redis_client.get(AccountService._get_account_refresh_token_key(account.id))
refresh_token = redis_client.get(
AccountService._get_account_refresh_token_key(account.id)
)
if refresh_token:
AccountService._delete_refresh_token(refresh_token.decode("utf-8"), account.id)
AccountService._delete_refresh_token(
refresh_token.decode("utf-8"), account.id
)
@staticmethod
def refresh_token(refresh_token: str) -> TokenPair:
# Verify the refresh token
account_id = redis_client.get(AccountService._get_refresh_token_key(refresh_token))
account_id = redis_client.get(
AccountService._get_refresh_token_key(refresh_token)
)
if not account_id:
raise ValueError("Invalid refresh token")
@ -443,7 +471,9 @@ class AccountService:
raise ValueError("Email must be provided.")
if cls.reset_password_rate_limiter.is_rate_limited(account_email):
from controllers.console.auth.error import PasswordResetRateLimitExceededError
from controllers.console.auth.error import (
PasswordResetRateLimitExceededError,
)
raise PasswordResetRateLimitExceededError()
@ -469,7 +499,10 @@ class AccountService:
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
additional_data["code"] = code
token = TokenManager.generate_token(
account=account, email=email, token_type="reset_password", additional_data=additional_data
account=account,
email=email,
token_type="reset_password",
additional_data=additional_data,
)
return code, token
@ -492,10 +525,14 @@ class AccountService:
if email is None:
raise ValueError("Email must be provided.")
if dify_config.DEBUG_ORG_EMAIL_DOMAIN and email.endswith(dify_config.DEBUG_ORG_EMAIL_DOMAIN):
if dify_config.DEBUG_ORG_EMAIL_DOMAIN and email.endswith(
dify_config.DEBUG_ORG_EMAIL_DOMAIN
):
code = dify_config.DEBUG_CODE_FOR_LOGIN
elif 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()
else:
@ -622,7 +659,9 @@ class AccountService:
redis_client.setex(freeze_key, 60 * 60, 1)
return True
else:
redis_client.setex(hour_limit_key, 60 * 10, hour_limit_count + 1) # first time limit 10 minutes
redis_client.setex(
hour_limit_key, 60 * 10, hour_limit_count + 1
) # first time limit 10 minutes
# add hour limit count
redis_client.incr(hour_limit_key)
@ -647,11 +686,7 @@ class AccountService:
Raises Unauthorized if account is banned.
"""
# Query directly with phone number first
admin_account = (
db.session.query(Account)
.filter(Account.phone == phone)
.first()
)
admin_account = db.session.query(Account).filter(Account.phone == phone).first()
if not admin_account:
return None
@ -662,7 +697,9 @@ class AccountService:
organization_id = admin_account.current_organization_id
if not organization_id:
logging.warning(f"Account {admin_account.id} is not a member of any organization.")
logging.warning(
f"Account {admin_account.id} is not a member of any organization."
)
return None
# If organization_id is provided, check if account is an admin member of that organization
@ -673,26 +710,28 @@ class AccountService:
.filter(
OrganizationMember.organization_id == organization_id,
OrganizationMember.account_id == admin_account.id,
OrganizationMember.role == OrganizationRole.ADMIN
OrganizationMember.role == OrganizationRole.ADMIN,
)
.first()
)
if not org_member:
logging.warning(f"Account {admin_account.id} is not a member of any organization.")
logging.warning(
f"Account {admin_account.id} is not a member of any organization."
)
return None
return admin_account
@classmethod
def is_phone_send_ip_limit(cls, ip_address: str) -> bool:
def is_login_attempt_ip_limit(cls, ip_address: str) -> bool:
"""
Check if IP has reached the limit for sending phone verification codes.
Similar to is_email_send_ip_limit but for phone verification.
"""
minute_key = f"phone_send_ip_limit_minute:{ip_address}"
freeze_key = f"phone_send_ip_limit_freeze:{ip_address}"
hour_limit_key = f"phone_send_ip_limit_hour:{ip_address}"
minute_key = f"login_attempt_ip_limit_minute:{ip_address}"
freeze_key = f"login_attempt_ip_limit_freeze:{ip_address}"
hour_limit_key = f"login_attempt_ip_limit_hour:{ip_address}"
# check ip is frozen
if redis_client.get(freeze_key):
@ -705,7 +744,9 @@ class AccountService:
current_minute_count = int(current_minute_count)
# check current hour count
if current_minute_count > dify_config.EMAIL_SEND_IP_LIMIT_PER_MINUTE: # Use same limit as email
if (
current_minute_count > dify_config.EMAIL_SEND_IP_LIMIT_PER_MINUTE
): # Use same limit as email
hour_limit_count = redis_client.get(hour_limit_key)
if hour_limit_count is None:
hour_limit_count = 0
@ -715,7 +756,9 @@ class AccountService:
redis_client.setex(freeze_key, 60 * 60, 1)
return True
else:
redis_client.setex(hour_limit_key, 60 * 10, hour_limit_count + 1) # first time limit 10 minutes
redis_client.setex(
hour_limit_key, 60 * 10, hour_limit_count + 1
) # first time limit 10 minutes
# add hour limit count
redis_client.incr(hour_limit_key)
@ -769,6 +812,34 @@ class AccountService:
"""Revoke phone code login token"""
TokenManager.revoke_token(token, "phone_code_login")
@classmethod
def get_admin_through_login_id(cls, login_id: str):
"""
Get admin account through login ID (either email or phone number).
Args:
login_id: The email or phone number to search for
Returns None if no admin account with this ID exists.
Raises Unauthorized if account is banned.
"""
account = (
db.session.query(Account)
.filter((Account.email == login_id) | (Account.phone == login_id))
.first()
)
if not account:
return None
if account.status == AccountStatus.BANNED.value:
raise Unauthorized("Account is banned.")
if not account.is_org_admin():
return None
return account
class TenantService:
@ -806,38 +877,53 @@ class TenantService:
):
"""Check if user have a workspace or not"""
available_ta = (
TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first()
TenantAccountJoin.query.filter_by(account_id=account.id)
.order_by(TenantAccountJoin.id.asc())
.first()
)
if available_ta:
return
"""Create owner tenant if not exist"""
if not FeatureService.get_system_features().is_allow_create_workspace and not is_setup:
if (
not FeatureService.get_system_features().is_allow_create_workspace
and not is_setup
):
raise WorkSpaceNotAllowedCreateError()
if name:
tenant = TenantService.create_tenant(name=name, is_setup=is_setup)
else:
tenant = TenantService.create_tenant(name=f"{account.name}'s Workspace", is_setup=is_setup)
tenant = TenantService.create_tenant(
name=f"{account.name}'s Workspace", is_setup=is_setup
)
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
db.session.commit()
tenant_was_created.send(tenant)
@staticmethod
def create_tenant_member(tenant: Tenant, account: Account, role: str = "normal") -> TenantAccountJoin:
def create_tenant_member(
tenant: Tenant, account: Account, role: str = "normal"
) -> TenantAccountJoin:
"""Create tenant member"""
if role == TenantAccountRole.OWNER.value:
if TenantService.has_roles(tenant, [TenantAccountRole.OWNER]):
logging.error(f"Tenant {tenant.id} has already an owner.")
raise Exception("Tenant already has an owner.")
ta = db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=account.id).first()
ta = (
db.session.query(TenantAccountJoin)
.filter_by(tenant_id=tenant.id, account_id=account.id)
.first()
)
if ta:
ta.role = role
else:
ta = TenantAccountJoin(tenant_id=tenant.id, account_id=account.id, role=role)
ta = TenantAccountJoin(
tenant_id=tenant.id, account_id=account.id, role=role
)
db.session.add(ta)
db.session.commit()
@ -863,7 +949,9 @@ class TenantService:
if not tenant:
raise TenantNotFoundError("Tenant not found.")
ta = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=account.id).first()
ta = TenantAccountJoin.query.filter_by(
tenant_id=tenant.id, account_id=account.id
).first()
if ta:
tenant.role = ta.role
else:
@ -890,7 +978,9 @@ class TenantService:
)
if not tenant_account_join:
raise AccountNotLinkTenantError("Tenant not found or account is not a member of the tenant.")
raise AccountNotLinkTenantError(
"Tenant not found or account is not a member of the tenant."
)
else:
TenantAccountJoin.query.filter(
TenantAccountJoin.account_id == account.id,
@ -975,7 +1065,9 @@ class TenantService:
return cast(int, db.session.query(func.count(Tenant.id)).scalar())
@staticmethod
def check_member_permission(tenant: Tenant, operator: Account, member: Account | None, action: str) -> None:
def check_member_permission(
tenant: Tenant, operator: Account, member: Account | None, action: str
) -> None:
"""Check member permission"""
perms = {
"add": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
@ -989,20 +1081,26 @@ class TenantService:
if operator.id == member.id:
raise CannotOperateSelfError("Cannot operate self.")
ta_operator = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=operator.id).first()
ta_operator = TenantAccountJoin.query.filter_by(
tenant_id=tenant.id, account_id=operator.id
).first()
if not ta_operator or ta_operator.role not in perms[action]:
raise NoPermissionError(f"No permission to {action} member.")
@staticmethod
def remove_member_from_tenant(tenant: Tenant, account: Account, operator: Account) -> None:
def remove_member_from_tenant(
tenant: Tenant, account: Account, operator: Account
) -> None:
"""Remove member from tenant"""
if operator.id == account.id:
raise CannotOperateSelfError("Cannot operate self.")
TenantService.check_member_permission(tenant, operator, account, "remove")
ta = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=account.id).first()
ta = TenantAccountJoin.query.filter_by(
tenant_id=tenant.id, account_id=account.id
).first()
if not ta:
raise MemberNotInTenantError("Member not in tenant.")
@ -1010,18 +1108,26 @@ class TenantService:
db.session.commit()
@staticmethod
def update_member_role(tenant: Tenant, member: Account, new_role: str, operator: Account) -> None:
def update_member_role(
tenant: Tenant, member: Account, new_role: str, operator: Account
) -> None:
"""Update member role"""
TenantService.check_member_permission(tenant, operator, member, "update")
target_member_join = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=member.id).first()
target_member_join = TenantAccountJoin.query.filter_by(
tenant_id=tenant.id, account_id=member.id
).first()
if target_member_join.role == new_role:
raise RoleAlreadyAssignedError("The provided role is already assigned to the member.")
raise RoleAlreadyAssignedError(
"The provided role is already assigned to the member."
)
if new_role == "owner":
# Find the current owner and change their role to 'admin'
current_owner_join = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, role="owner").first()
current_owner_join = TenantAccountJoin.query.filter_by(
tenant_id=tenant.id, role="owner"
).first()
current_owner_join.role = "admin"
# Update the role of the target member
@ -1031,7 +1137,9 @@ class TenantService:
@staticmethod
def dissolve_tenant(tenant: Tenant, operator: Account) -> None:
"""Dissolve tenant"""
if not TenantService.check_member_permission(tenant, operator, operator, "remove"):
if not TenantService.check_member_permission(
tenant, operator, operator, "remove"
):
raise NoPermissionError("No permission to dissolve tenant.")
db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id).delete()
db.session.delete(tenant)
@ -1116,7 +1224,10 @@ class RegisterService:
if open_id is not None and provider is not None:
AccountService.link_account_integrate(provider, open_id, account)
if FeatureService.get_system_features().is_allow_create_workspace and create_workspace_required:
if (
FeatureService.get_system_features().is_allow_create_workspace
and create_workspace_required
):
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
@ -1140,7 +1251,12 @@ class RegisterService:
@classmethod
def invite_new_member(
cls, tenant: Tenant, email: str, language: str, role: str = "normal", inviter: Account | None = None
cls,
tenant: Tenant,
email: str,
language: str,
role: str = "normal",
inviter: Account | None = None,
) -> str:
if not inviter:
raise ValueError("Inviter is required")
@ -1165,7 +1281,9 @@ class RegisterService:
TenantService.switch_tenant(account, tenant.id)
else:
TenantService.check_member_permission(tenant, inviter, account, "add")
ta = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=account.id).first()
ta = TenantAccountJoin.query.filter_by(
tenant_id=tenant.id, account_id=account.id
).first()
if not ta:
TenantService.create_tenant_member(tenant, account, role)
@ -1212,7 +1330,9 @@ class RegisterService:
def revoke_token(cls, workspace_id: str, email: str, token: str):
if workspace_id and email:
email_hash = sha256(email.encode()).hexdigest()
cache_key = "member_invite_token:{}, {}:{}".format(workspace_id, email_hash, token)
cache_key = "member_invite_token:{}, {}:{}".format(
workspace_id, email_hash, token
)
redis_client.delete(cache_key)
else:
redis_client.delete(cls._get_invitation_token_key(token))
@ -1227,7 +1347,9 @@ class RegisterService:
tenant = (
db.session.query(Tenant)
.filter(Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal")
.filter(
Tenant.id == invitation_data["workspace_id"], Tenant.status == "normal"
)
.first()
)

@ -1079,6 +1079,7 @@ IMAGE_GENERATION_APP_ID=
DEBUG_ORG_EMAIL_DOMAIN=test.edu
DEBUG_CODE_FOR_LOGIN=
DEBUG_ADMIN_PHONE=
DEBUG_ADMIN_EMAIL=
PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120
PLUGIN_MAX_EXECUTION_TIMEOUT=600

@ -476,6 +476,7 @@ x-shared-env: &shared-api-worker-env
DEBUG_ORG_EMAIL_DOMAIN: ${DEBUG_ORG_EMAIL_DOMAIN:-test.edu}
DEBUG_CODE_FOR_LOGIN: ${DEBUG_CODE_FOR_LOGIN:-}
DEBUG_ADMIN_PHONE: ${DEBUG_ADMIN_PHONE:-}
DEBUG_ADMIN_EMAIL: ${DEBUG_ADMIN_EMAIL:-}
PLUGIN_PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}

Loading…
Cancel
Save