120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from flask import Flask, abort, jsonify, request
|
|
|
|
from .config import Config, DATA_DIR, ensure_dirs
|
|
from .extensions import db, login_manager
|
|
from .models import AppSetting, User
|
|
from .prediction import FEATURES, load_model
|
|
from .security import csrf_token, validate_csrf_token
|
|
|
|
|
|
def create_app(config_object: type[Config] = Config, *, load_model_on_start: bool = True) -> Flask:
|
|
ensure_dirs()
|
|
configure_logging()
|
|
|
|
app = Flask(__name__, template_folder="../templates", static_folder="../static")
|
|
app.config.from_object(config_object)
|
|
|
|
if app.config.get("SECRET_KEY_GENERATED"):
|
|
logging.warning("未设置 SECRET_KEY,已生成临时密钥;服务重启后登录会话将失效。")
|
|
|
|
db.init_app(app)
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = "main.login"
|
|
login_manager.login_message = "请先登录后再访问该页面。"
|
|
|
|
register_app_hooks(app)
|
|
|
|
from .routes import bp
|
|
|
|
app.register_blueprint(bp)
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
init_admin_user(app)
|
|
|
|
if load_model_on_start:
|
|
try:
|
|
app.config["RSF_MODEL"] = load_model(app.config["MODEL_PATH"])
|
|
logging.info("模型加载成功")
|
|
except Exception as exc:
|
|
app.config["RSF_MODEL"] = None
|
|
logging.exception("模型加载失败: %s", exc)
|
|
|
|
return app
|
|
|
|
|
|
def configure_logging() -> None:
|
|
logging.basicConfig(
|
|
filename=str(DATA_DIR / "app.log"),
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
)
|
|
|
|
|
|
def init_admin_user(app: Flask) -> None:
|
|
admin_username = app.config["ADMIN_USERNAME"]
|
|
admin_password = app.config["ADMIN_PASSWORD"]
|
|
if admin_password:
|
|
admin = User.query.filter_by(username=admin_username).first()
|
|
if admin is None:
|
|
admin = User(username=admin_username, is_admin=True)
|
|
admin.set_password(admin_password)
|
|
db.session.add(admin)
|
|
else:
|
|
admin.is_admin = True
|
|
if not admin.check_password(admin_password):
|
|
admin.set_password(admin_password)
|
|
db.session.commit()
|
|
elif not User.query.filter_by(is_admin=True).first():
|
|
logging.warning("未设置 ADMIN_PASSWORD,跳过自动创建管理员账号。")
|
|
|
|
default_admin = User.query.filter_by(username="admin", is_admin=True).first()
|
|
if default_admin and default_admin.check_password("admin123"):
|
|
logging.warning("检测到默认管理员密码 admin123,请立即通过 ADMIN_PASSWORD 更新。")
|
|
|
|
|
|
def register_app_hooks(app: Flask) -> None:
|
|
@login_manager.user_loader
|
|
def load_user(user_id: str):
|
|
try:
|
|
return db.session.get(User, int(user_id))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
@app.context_processor
|
|
def inject_helpers():
|
|
def now_year() -> int:
|
|
return datetime.now().year
|
|
|
|
return {
|
|
"feature_list": FEATURES,
|
|
"now_year": now_year,
|
|
"csrf_token": csrf_token,
|
|
"allow_registration": AppSetting.get_bool(
|
|
"allow_registration",
|
|
app.config["ALLOW_REGISTRATION"],
|
|
),
|
|
}
|
|
|
|
@app.errorhandler(413)
|
|
def handle_file_too_large(_exc):
|
|
max_mb = app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024)
|
|
if request.path == "/predict":
|
|
return jsonify({"error": f"文件过大,请上传 {max_mb}MB 以内的文件。"}), 413
|
|
return f"文件过大,请上传 {max_mb}MB 以内的文件。", 413
|
|
|
|
@app.before_request
|
|
def protect_csrf():
|
|
if request.method not in {"POST", "PUT", "PATCH", "DELETE"}:
|
|
return None
|
|
if validate_csrf_token():
|
|
return None
|
|
if request.path == "/predict" or request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
|
return jsonify({"error": "CSRF 校验失败,请刷新页面后重试。"}), 400
|
|
abort(400)
|