Files
microfish/backend/app/__init__.py
Kunthawat Greethong 91beb8c2dc fix: fail fast on missing/short SECRET_KEY; hide ADMIN_PASSWORD from argv
Production correct-credential login returned auth_unavailable 503 because
Flask's SECRET_KEY was unset: wrong-password probes stopped at 401 before
CSRF token issuance, while valid credentials reached _csrf_serializer()
and crashed. App factory now rejects absent/short (<32 char) SECRET_KEY at
startup, and docker_entrypoint.sh fails fast before migration/services.
Bootstrap no longer passes ADMIN_PASSWORD in process arguments; env-only.

Tests: app-factory + entrypoint regression (5 focused passed), full
backend suite 204 passed. Independent review PASS.
2026-09-01 16:28:04 +07:00

125 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
CrowdSight Backend - Flask应用工厂
"""
import os
import warnings
# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers
# 需要在所有其他导入之前设置
warnings.filterwarnings("ignore", message=".*resource_tracker.*")
from flask import Flask, jsonify, request
from flask_cors import CORS
from werkzeug.exceptions import HTTPException
from .config import Config
from .db import create_database_engine, create_session_factory
from .utils.api_errors import ApiError, internal_error_payload
from .utils.locale import t
from .utils.logger import setup_logger, get_logger
def create_app(config_class=Config):
"""Flask应用工厂函数"""
app = Flask(__name__)
app.config.from_object(config_class)
if not app.config.get("SECRET_KEY") or len(str(app.config.get("SECRET_KEY"))) < 32:
raise RuntimeError("secret_key_required")
if "*" in app.config.get("CORS_ALLOWED_ORIGINS", []):
raise RuntimeError("wildcard_cors_not_allowed")
database_engine = create_database_engine(os.environ.get("DATABASE_URL"))
app.extensions["crowdsight_database_engine"] = database_engine
app.extensions["crowdsight_session_factory"] = create_session_factory(database_engine)
# JSON config: keep Unicode characters readable in API responses.
# Flask >= 2.3 使用 app.json.ensure_ascii旧版本使用 JSON_AS_ASCII 配置
if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'):
app.json.ensure_ascii = False
# Configure server-side logging before registering error handlers.
logger = setup_logger('crowdsight')
@app.errorhandler(ApiError)
def handle_api_error(error: ApiError):
return jsonify(error.to_payload(t)), error.status_code
@app.errorhandler(HTTPException)
def handle_http_error(error: HTTPException):
logger.warning("HTTP request failed: status=%s", error.code)
api_error = ApiError(
code=f"http_{error.code or 500}",
status_code=error.code or 500,
message_key="api.requestError",
)
return jsonify(api_error.to_payload(t)), error.code or 500
@app.errorhandler(Exception)
def handle_unexpected_error(error: Exception):
# Keep exception details in server logs only; never serialize them.
logger.exception("Unhandled request error: %s", type(error).__name__)
return jsonify(internal_error_payload(t)), 500
# Only startup state is logged; request bodies are intentionally excluded.
is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
debug_mode = app.config.get('DEBUG', False)
should_log_startup = not debug_mode or is_reloader_process
if should_log_startup:
logger.info("=" * 50)
logger.info("CrowdSight Backend 启动中...")
logger.info("=" * 50)
# Explicit allowlist only; wildcard CORS is incompatible with auth cookies.
CORS(
app,
resources={r"/api/*": {"origins": app.config.get("CORS_ALLOWED_ORIGINS", [])}},
supports_credentials=True,
)
# 注册模拟进程清理函数(确保服务器关闭时终止所有模拟进程)
from .services.simulation_runner import SimulationRunner
SimulationRunner.register_cleanup()
if should_log_startup:
logger.info("已注册模拟进程清理函数")
# 请求日志中间件
@app.before_request
def log_request():
logger = get_logger('crowdsight.request')
logger.debug(f"请求: {request.method} {request.path}")
if request.content_type and 'json' in request.content_type:
logger.debug("JSON request received: path=%s", request.path)
@app.after_request
def log_response(response):
logger = get_logger('crowdsight.request')
logger.debug(f"响应: {response.status_code}")
return response
# 注册蓝图
from .api import graph_bp, simulation_bp, report_bp
from .api.admin import admin_bp
from .api.auth import auth_bp
from .api.template import template_bp
from .api.agent_group import agent_group_bp
app.register_blueprint(graph_bp, url_prefix='/api/graph')
app.register_blueprint(auth_bp, url_prefix='/api/auth')
app.register_blueprint(admin_bp, url_prefix='/api/admin')
app.register_blueprint(simulation_bp, url_prefix='/api/simulation')
app.register_blueprint(report_bp, url_prefix='/api/report')
app.register_blueprint(template_bp, url_prefix='/api/template')
app.register_blueprint(agent_group_bp, url_prefix='/api/agent-group')
# 健康检查
@app.route('/health')
def health():
return {'status': 'ok', 'service': 'CrowdSight Backend'}
if should_log_startup:
logger.info("CrowdSight Backend 启动完成")
return app