- Refactor 6000+ line web_server.py into server/ module - Create separate modules: auth, chat, conversation, files, admin, etc. - Keep web_server.py as backward-compatible entry point - Add container running status field in user_container_manager - Improve admin dashboard API with credentials and debug support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
242 lines
8.9 KiB
Python
242 lines
8.9 KiB
Python
from __future__ import annotations
|
|
import mimetypes
|
|
from pathlib import Path
|
|
from flask import Blueprint, request, jsonify, session, redirect, send_from_directory, abort, current_app
|
|
|
|
from modules.personalization_manager import load_personalization_config
|
|
from modules.user_manager import UserWorkspace
|
|
|
|
from .auth_helpers import login_required, api_login_required, get_current_user_record, get_current_username
|
|
from .security import (
|
|
get_csrf_token,
|
|
check_rate_limit,
|
|
register_failure,
|
|
is_action_blocked,
|
|
clear_failures,
|
|
)
|
|
from .context import with_terminal, get_gui_manager
|
|
from . import state
|
|
from .utils_common import debug_log
|
|
|
|
auth_bp = Blueprint("auth", __name__)
|
|
|
|
|
|
@auth_bp.route('/api/csrf-token', methods=['GET'])
|
|
def issue_csrf_token():
|
|
token = get_csrf_token()
|
|
response = jsonify({"success": True, "token": token})
|
|
response.headers['Cache-Control'] = 'no-store'
|
|
return response
|
|
|
|
|
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'GET':
|
|
if session.get('username'):
|
|
return redirect('/new')
|
|
if not state.container_manager.has_capacity():
|
|
return current_app.send_static_file('resource_busy.html'), 503
|
|
return current_app.send_static_file('login.html')
|
|
|
|
data = request.get_json() or {}
|
|
email = (data.get('email') or '').strip()
|
|
password = data.get('password') or ''
|
|
client_ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip() or request.remote_addr or 'unknown'
|
|
|
|
limited, retry_after = check_rate_limit("login", 10, 60, client_ip)
|
|
if limited:
|
|
return jsonify({"success": False, "error": "登录请求过于频繁,请稍后再试。", "retry_after": retry_after}), 429
|
|
|
|
blocked, block_for = is_action_blocked("login", identifier=client_ip)
|
|
if blocked:
|
|
return jsonify({"success": False, "error": f"尝试次数过多,请 {block_for} 秒后重试。", "retry_after": block_for}), 429
|
|
|
|
record = state.user_manager.authenticate(email, password)
|
|
if not record:
|
|
wait_seconds = register_failure("login", state.FAILED_LOGIN_LIMIT, state.FAILED_LOGIN_LOCK_SECONDS, identifier=client_ip)
|
|
error_payload = {"success": False, "error": "账号或密码错误"}
|
|
status_code = 401
|
|
if wait_seconds:
|
|
error_payload.update({"error": f"尝试次数过多,请 {wait_seconds} 秒后重试。", "retry_after": wait_seconds})
|
|
status_code = 429
|
|
return jsonify(error_payload), status_code
|
|
|
|
workspace = state.user_manager.ensure_user_workspace(record.username)
|
|
preferred_run_mode = None
|
|
try:
|
|
personal_config = load_personalization_config(workspace.data_dir)
|
|
candidate_mode = (personal_config or {}).get('default_run_mode')
|
|
if isinstance(candidate_mode, str):
|
|
normalized_mode = candidate_mode.lower()
|
|
if normalized_mode in {"fast", "thinking", "deep"}:
|
|
preferred_run_mode = normalized_mode
|
|
except Exception as exc:
|
|
debug_log(f"加载个性化偏好失败: {exc}")
|
|
|
|
session['logged_in'] = True
|
|
session['username'] = record.username
|
|
session['role'] = record.role or 'user'
|
|
default_thinking = current_app.config.get('DEFAULT_THINKING_MODE', False)
|
|
session['thinking_mode'] = default_thinking
|
|
session['run_mode'] = current_app.config.get('DEFAULT_RUN_MODE', "deep" if default_thinking else "fast")
|
|
if preferred_run_mode:
|
|
session['run_mode'] = preferred_run_mode
|
|
session['thinking_mode'] = preferred_run_mode != 'fast'
|
|
session.permanent = True
|
|
clear_failures("login", identifier=client_ip)
|
|
try:
|
|
state.container_manager.ensure_container(record.username, str(workspace.project_path))
|
|
except RuntimeError as exc:
|
|
session.clear()
|
|
return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503
|
|
from .usage import record_user_activity
|
|
record_user_activity(record.username)
|
|
get_csrf_token(force_new=True)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
|
def register():
|
|
if request.method == 'GET':
|
|
if session.get('username'):
|
|
return redirect('/new')
|
|
return current_app.send_static_file('register.html')
|
|
|
|
data = request.get_json() or {}
|
|
username = (data.get('username') or '').strip()
|
|
email = (data.get('email') or '').strip()
|
|
password = data.get('password') or ''
|
|
invite_code = (data.get('invite_code') or '').strip()
|
|
|
|
from .security import get_client_ip
|
|
limited, retry_after = check_rate_limit("register", 5, 300, get_client_ip())
|
|
if limited:
|
|
return jsonify({"success": False, "error": "注册请求过于频繁,请稍后再试。", "retry_after": retry_after}), 429
|
|
try:
|
|
state.user_manager.register_user(username, email, password, invite_code)
|
|
return jsonify({"success": True})
|
|
except ValueError as exc:
|
|
return jsonify({"success": False, "error": str(exc)}), 400
|
|
except Exception as exc:
|
|
return jsonify({"success": False, "error": str(exc)}), 500
|
|
|
|
|
|
@auth_bp.route('/logout', methods=['POST'])
|
|
def logout():
|
|
username = session.get('username')
|
|
session.clear()
|
|
if username and username in state.user_terminals:
|
|
state.user_terminals.pop(username, None)
|
|
if username:
|
|
state.container_manager.release_container(username, reason="logout")
|
|
for token_value, meta in list(state.pending_socket_tokens.items()):
|
|
if meta.get("username") == username:
|
|
state.pending_socket_tokens.pop(token_value, None)
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@auth_bp.route('/')
|
|
@login_required
|
|
def index():
|
|
return redirect('/new')
|
|
|
|
|
|
@auth_bp.route('/new')
|
|
@login_required
|
|
def new_page():
|
|
return current_app.send_static_file('index.html')
|
|
|
|
|
|
@auth_bp.route('/<conv:conversation_id>')
|
|
@login_required
|
|
def conversation_page(conversation_id):
|
|
return current_app.send_static_file('index.html')
|
|
|
|
|
|
@auth_bp.route('/terminal')
|
|
@login_required
|
|
def terminal_page():
|
|
from .auth_helpers import resolve_admin_policy
|
|
policy = resolve_admin_policy(get_current_user_record())
|
|
if policy.get("ui_blocks", {}).get("block_realtime_terminal"):
|
|
return "实时终端已被管理员禁用", 403
|
|
return current_app.send_static_file('terminal.html')
|
|
|
|
|
|
@auth_bp.route('/file-manager')
|
|
@login_required
|
|
def gui_file_manager_page():
|
|
from .auth_helpers import resolve_admin_policy
|
|
policy = resolve_admin_policy(get_current_user_record())
|
|
if policy.get("ui_blocks", {}).get("block_file_manager"):
|
|
return "文件管理器已被管理员禁用", 403
|
|
return send_from_directory(Path(current_app.static_folder) / 'file_manager', 'index.html')
|
|
|
|
|
|
@auth_bp.route('/file-manager/editor')
|
|
@login_required
|
|
def gui_file_editor_page():
|
|
return send_from_directory(Path(current_app.static_folder) / 'file_manager', 'editor.html')
|
|
|
|
|
|
@auth_bp.route('/file-preview/<path:relative_path>')
|
|
@login_required
|
|
@with_terminal
|
|
def gui_file_preview(relative_path: str, terminal, workspace: UserWorkspace, username: str):
|
|
manager = get_gui_manager(workspace)
|
|
try:
|
|
target = manager.prepare_download(relative_path)
|
|
if not target.is_file():
|
|
return "预览仅支持文件", 400
|
|
return send_from_directory(directory=target.parent, path=target.name, mimetype='text/html')
|
|
except Exception as exc:
|
|
return f"无法预览文件: {exc}", 400
|
|
|
|
|
|
@auth_bp.route('/user_upload/<path:filename>')
|
|
@login_required
|
|
def serve_user_upload(filename: str):
|
|
user = get_current_user_record()
|
|
if not user:
|
|
return redirect('/login')
|
|
workspace = state.user_manager.ensure_user_workspace(user.username)
|
|
uploads_dir = workspace.uploads_dir.resolve()
|
|
target = (uploads_dir / filename).resolve()
|
|
try:
|
|
target.relative_to(uploads_dir)
|
|
except ValueError:
|
|
abort(403)
|
|
if not target.exists() or not target.is_file():
|
|
abort(404)
|
|
return send_from_directory(str(uploads_dir), str(target.relative_to(uploads_dir)))
|
|
|
|
|
|
@auth_bp.route('/workspace/<path:filename>')
|
|
@login_required
|
|
def serve_workspace_file(filename: str):
|
|
user = get_current_user_record()
|
|
if not user:
|
|
return redirect('/login')
|
|
workspace = state.user_manager.ensure_user_workspace(user.username)
|
|
project_root = workspace.project_path.resolve()
|
|
target = (project_root / filename).resolve()
|
|
try:
|
|
target.relative_to(project_root)
|
|
except ValueError:
|
|
abort(403)
|
|
if not target.exists() or not target.is_file():
|
|
abort(404)
|
|
mime_type, _ = mimetypes.guess_type(str(target))
|
|
if not mime_type or not mime_type.startswith("image/"):
|
|
abort(415)
|
|
return send_from_directory(str(target.parent), target.name)
|
|
|
|
|
|
@auth_bp.route('/static/<path:filename>')
|
|
def static_files(filename):
|
|
if filename.startswith('admin_dashboard'):
|
|
abort(404)
|
|
return send_from_directory('static', filename)
|
|
|
|
__all__ = ["auth_bp"]
|