Account Service Layer (services/account_service.py)
The AccountService class provides static and class methods for core user lifecycle management:
- User Retrieval:
load_user(user_id)fetches a user by ID. It raises an exception if the account is banned or closed. If the user has a tenant, it sets the active tenant and updates the last active timestamp. - Authentication:
authenticate(email, password)validates credentials and raises an error on failure. - Password Management:
update_account_password(account, old_pwd, new_pwd)verifies the old password, generates a new salt usingsecrets, and stores the hashed password. - Registration:
create_account(email, name, language, password, theme)persists a new user and sets the timezone based on language. - JWT & Cache:
login(account, ip)updates login metadata, generates a JWT (containing user ID, expiration, and issuer), and stores the token in Redis.logout(account, token)removes the token from Redis.load_logged_in_account(account_id, token)validates the session via cache. - Password Reset: Class methods like
send_reset_password_email(account)handle token generation with rate limiting, whilerevoke_reset_password_tokenandget_reset_password_datamanage token lifecycle. - Third-Party Integration:
link_account_integrate(provider, open_id, account)links social logins.
Security is enforced via hash_password for storage and compare_password for verification. Tokens are primarily handled using JWT for stateless auth and Redis for session validation.
Tenant Management (TenantService)
TenantService handles multi-tenancy logic:
- Tenant Lifecycle:
create_tenant(name)generates encryption keys.create_owner_tenant_if_not_exist(account, name)ensures a user has an owned workspace. - Membership:
create_tenant_member(tenant, account, role)assigns roles.get_join_tenants(account)lists associated workspaces. - Context Switching:
switch_tenant(account, tenant_id)updates the active workspace for a user. - Role Checks:
get_user_role(account, tenant)andhas_roles(tenant, roles)enforce RBAC. - Administration:
remove_member_from_tenant,update_member_role, anddissolve_tenanthandle governance.check_member_permissionvalidates if an operator can modify another member.
Registration and Envitation (RegisterService)
This service orchestrates user onboarding:
- Initial Setup:
setup(email, name, password, ip)creates the first admin account and workspace, rolling back on failure. - Standard Registration:
register(...)handles direct sign-ups with optional third-party integration. - Invitation Flow:
invite_new_member(tenant, email, language, role, inviter)sends emails and creates pending users.generate_invite_token(tenant, account)stores invitation data in Redis.get_invitation_if_token_valid(workspace_id, email, token)validates tokens before activation.
Custom Signup Endpoint (CustomSignUpApi)
A specialized API resource for automated member registration:
import re
import json
import requests
from flask import jsonify
from flask_restful import Resource
from controllers.console import api
class CustomMemberSignUpApi(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('email', type=str, required=True)
parser.add_argument('user_name', type=str, required=True)
parser.add_argument('password', type=str, required=True)
args = parser.parse_args()
if not self._is_password_strong(args['password']):
return jsonify({"error": "Password must be 8+ chars with letters and numbers."}), 400
# 1. Admin Login
admin_auth = requests.post('http://localhost/console/api/login', json={
"email": "admin@domain.com",
"password": "SecurePass123",
"remember_me": True
})
if admin_auth.status_code != 200:
return jsonify({"error": "Admin auth failed"}), 401
admin_token = json.loads(admin_auth.text)['data']
headers = {"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}
# 2. Invite Member
invite_resp = requests.post(
'http://localhost/console/api/workspaces/current/members/invite-email',
headers=headers,
json={"emails": [args['email']], "role": "normal", "language": "en-US"}
)
if invite_resp.status_code != 201:
return jsonify({"error": "Invitation failed"}), 400
invite_data = json.loads(invite_resp.text)
try:
token = invite_data['invitation_results'][0]['url'].split("token=")[1]
except (KeyError, IndexError):
return jsonify({"error": "User may already exist"}), 400
# 3. Activate Account
activate_resp = requests.post(
'http://localhost/console/api/activate',
headers=headers,
json={
"email": args['email'],
"name": args['user_name'],
"password": args['password'],
"token": token,
"timezone": "Asia/Shanghai",
"interface_language": "en-US"
}
)
if activate_resp.status_code != 200:
return jsonify({"error": "Activation failed"}), activate_resp.status_code
return json.loads(activate_resp.text), 200
def _is_password_strong(self, pwd):
return len(pwd) >= 8 and re.search(r"[A-Za-z]", pwd) and re.search(r"[0-9]", pwd)
api.add_resource(CustomMemberSignUpApi, '/workspaces/current/members/custom_signup')
Access Control Decorators (controllers/console/wraps.py)
These decorators protect API endpoints:
account_initialization_required: Ensurescurrent_user.statusis not "uninitialized".only_edition_cloud/only_edition_self_hosted: Restricts access based ondify_config.EDITION.cloud_edition_billing_resource_check(resource): Checks quota limits (members, apps, vector space, documents) against the subscription plan. Returns 403 if limits are reached.cloud_edition_billing_knowledge_limit_check: Prevents sandbox users from adding segments.cloud_utm_record: Captures UTM cookies for marketing analytics.
Initial Setup Endpoint (controllers/console/setup.py)
SetupApi manages the first-time configuration:
from flask_restful import Resource, reqparse
from functools import wraps
from configs import dify_config
from services.account_service import RegisterService, TenantService
from models.model import DifySetup
from .wraps import only_edition_self_hosted
class SetupApi(Resource):
def get(self):
if dify_config.EDITION == "SELF_HOSTED":
status = DifySetup.query.first()
if status:
return {"step": "finished", "setup_at": status.setup_at.isoformat()}
return {"step": "not_started"}
return {"step": "finished"}
@only_edition_self_hosted
def post(self):
if DifySetup.query.first() or TenantService.get_tenant_count() > 0:
raise AlreadySetupError()
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("name", type=StrLen(30), required=True, location="json")
parser.add_argument("password", type=valid_password, required=True, location="json")
args = parser.parse_args()
RegisterService.setup(
email=args["email"],
name=args["name"],
password=args["password"],
ip_address=get_remote_ip(request)
)
return {"result": "success"}, 201
api.add_resource(SetupApi, "/setup")
def setup_required(view):
@wraps(view)
def decorated(*args, **kwargs):
if not get_setup_status():
raise NotSetupError()
return view(*args, **kwargs)
return decorated
Workspace Member Management (controllers/console/workspace/members.py)
Endpoints for managing workspace participants:
MemberListApi: Lists all members with roles.MemberInviteEmailApi: Sends batch invitations. Includes billing checks via@cloud_edition_billing_resource_check("members").MemberCancelInviteApi: Removes a member or cancels an invitation.MemberUpdateRoleApi: Modifies a member's role.DatasetOperatorMemberListApi: Lists users with dataset operation privileges.
Flask Basics
Flask is a micro-framework using WSGI, routing, and the Jinja2 template engine. Routes are defined using the @app.route decorator.
Basic Route Example:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello World!"
if __name__ == "__main__":
app.run()
Route with Parameters:
@app.route("/item/<int:item_id>")
def show_item(item_id):
return f"Item {item_id}"
Custom Converter:
from werkzeug.routing import BaseConverter
class MobileConverter(BaseConverter):
def __init__(self, url_map):
super().__init__(url_map)
self.regex = r'1\d{10}'
app.url_map.converters['mobile'] = MobileConverter
@app.route("/user/<mobile:phone>")
def user_phone(phone):
return f"Phone: {phone}"
Endpoint Mapping:
The url_map links URLs to endpoints, which map to view functions via view_functions.
Database Migration Issues
If flask db upgrade fails, verify the migration tool.
- Check for Flask-Migrate: Look for a
migrations/directory andflask_migratein requirements. - Initialize if missing:
flask db init flask db migrate -m "initial" flask db upgrade - If using raw Alembic: Ensure
alembic.iniexists andsqlalchemy.urlis configured correctly.
Redis Connection Troubleshooting
Error WinError 10061 indicates Redis is not running or refusing connections.
- Start Redis: Run
redis-serverin a terminal. - Verify Port: Use
netstat -ano | findstr 6379to confirm listening. - Test Connection: Use
redis-cli -h 127.0.0.1 -p 6379 ping. - Check Config: Ensure
redis.confhasbind 127.0.0.1andprotected-mode yes.
PowerShell API Testing
Windows PowerShell uses Invoke-RestMethod. Note that curl in PowerShell is an alias for Invoke-WebRequest, which requires headers as a dictionary.
Correct Syntax:
Invoke-RestMethod -Uri "http://localhost:5001/console/api/workspaces/current/members/custom_signup" `
-Method POST `
-Headers @{ "Content-Type" = "application/json" } `
-Body '{"email": "test@mail.com", "user_name": "test", "password": "Pass1234"}'
Common Error: TypeError: Object of type Response is not JSON serializable occurs when returning a requests.Response object directly. Always parse with .json() or json.loads() and return a dictionary.
Virtual Environment Listing:
- Conda:
conda info --envs - Virtualenvwrapper:
lsvirtualenv - Pyenv:
pyenv virtualenv-list - Manual: Check
~/.virtualenvsor project.venvfolders.