app_factory.py 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. from pathlib import Path
  2. from flask import Flask
  3. from flask import jsonify, request
  4. import secrets
  5. from .config import load_config
  6. from .db import ensure_default_admin, ensure_default_plans, init_db
  7. from .routes import register_routes
  8. def create_app() -> Flask:
  9. project_root = Path(__file__).resolve().parent.parent
  10. app = Flask(__name__, static_folder=str(project_root / "static"), template_folder=str(project_root / "templates"))
  11. config = load_config()
  12. app.config["APP_CONFIG"] = config
  13. app.secret_key = config.secret_key
  14. app.config.setdefault("SESSION_COOKIE_SAMESITE", "Lax")
  15. app.config.setdefault("SESSION_COOKIE_HTTPONLY", True)
  16. @app.before_request
  17. def _attach_config_to_g() -> None:
  18. from flask import g
  19. g.app_config = config
  20. @app.before_request
  21. def _csrf_protect() -> tuple[dict, int] | None:
  22. if request.method in {"GET", "HEAD", "OPTIONS", "TRACE"}:
  23. return None
  24. if request.path == "/pay/callback":
  25. return None
  26. if request.method != "POST":
  27. return None
  28. if (request.mimetype or "").lower() == "application/json":
  29. return None
  30. csrf_cookie = (request.cookies.get("csrf_token") or "").strip()
  31. csrf_header = (request.headers.get("X-CSRF-Token") or request.headers.get("X-Csrf-Token") or "").strip()
  32. if not csrf_cookie or not csrf_header or csrf_cookie != csrf_header:
  33. return jsonify({"error": "csrf_failed"}), 403
  34. return None
  35. @app.before_request
  36. def _block_writes_during_migration() -> tuple[dict, int] | None:
  37. if request.method in {"GET", "HEAD", "OPTIONS", "TRACE"}:
  38. return None
  39. from .db import is_migrating
  40. if not is_migrating():
  41. return None
  42. allow = {"/admin/db/switch", "/admin/db/status", "/admin/mysql/test"}
  43. if request.path in allow:
  44. return None
  45. return jsonify({"error": "readonly_migrating"}), 503
  46. @app.after_request
  47. def _set_csrf_cookie(resp):
  48. token = (request.cookies.get("csrf_token") or "").strip()
  49. if not token:
  50. token = secrets.token_urlsafe(32)
  51. resp.set_cookie("csrf_token", token, httponly=False, samesite="Lax", secure=bool(request.is_secure))
  52. return resp
  53. @app.teardown_appcontext
  54. def _close_db(_exc) -> None:
  55. from flask import g
  56. from .db import close_db
  57. db = g.pop("db", None)
  58. if db is not None:
  59. close_db(db)
  60. ctl_db = g.pop("ctl_db", None)
  61. if ctl_db is not None:
  62. try:
  63. ctl_db.close()
  64. except Exception:
  65. pass
  66. with app.app_context():
  67. init_db()
  68. ensure_default_admin()
  69. ensure_default_plans()
  70. register_routes(app)
  71. return app