85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""Application factory (Chapter 02 / Chapter 04)."""
|
|
from __future__ import annotations
|
|
|
|
from flask import Flask, jsonify, redirect, request, url_for
|
|
from flask_login import current_user
|
|
from sqlalchemy import text
|
|
|
|
from app.budget import validate_budget_config
|
|
from app.config import get_config
|
|
from app.extensions import cache, csrf, db, limiter, login_manager, migrate
|
|
from app.logging_setup import configure_logging
|
|
from app.utils.assets import register_asset_helper
|
|
|
|
|
|
def create_app(config_name: str | None = None) -> Flask:
|
|
"""Build and configure the Flask application instance."""
|
|
app = Flask(__name__)
|
|
app.config.from_object(get_config(config_name))
|
|
validate_budget_config(app) # Chapter 03: fail loudly on out-of-budget config
|
|
configure_logging(app) # Chapter 02 factor 11 / Chapter 12
|
|
|
|
if not app.config.get("SECRET_KEY") and not app.testing:
|
|
raise RuntimeError("SECRET_KEY must be set via environment variable")
|
|
|
|
db.init_app(app)
|
|
cache.init_app(app)
|
|
csrf.init_app(app)
|
|
login_manager.init_app(app)
|
|
migrate.init_app(app, db)
|
|
limiter.init_app(app) # Chapter 12: rate limiting
|
|
|
|
register_blueprints(app)
|
|
register_cli(app)
|
|
register_asset_helper(app)
|
|
|
|
@login_manager.unauthorized_handler
|
|
def handle_unauthorized():
|
|
"""Chapter 11 envelope compliance: JSON 401 for /api/... paths
|
|
instead of Flask-Login's default redirect (which would hand back
|
|
login HTML to a fetch()/HTMX JSON caller). Full-page routes still
|
|
redirect to the login form as normal.
|
|
"""
|
|
if request.path.startswith("/api/"):
|
|
return jsonify(error={"code": "unauthorized", "message": "Authentication required."}), 401
|
|
return redirect(url_for("auth.login", next=request.path))
|
|
|
|
@app.get("/healthz")
|
|
def healthz():
|
|
"""Liveness/readiness check (Chapter 12) — DB reachable, no long-running work."""
|
|
db.session.execute(text("SELECT 1"))
|
|
return {"data": {"status": "ok"}, "meta": {}}
|
|
|
|
@app.get("/")
|
|
def index():
|
|
"""Root URL: authenticated -> Overview, otherwise -> the login page."""
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for("overview.overview"))
|
|
return redirect(url_for("auth.login"))
|
|
|
|
return app
|
|
|
|
|
|
def register_blueprints(app: Flask) -> None:
|
|
"""Register one blueprint per dashboard section + support area (Ch. 04)."""
|
|
from app.blueprints.api import bp as api_bp
|
|
from app.blueprints.auth import bp as auth_bp
|
|
from app.blueprints.overview import bp as overview_bp
|
|
from app.blueprints.security import bp as security_bp
|
|
from app.blueprints.seo import bp as seo_bp
|
|
from app.blueprints.uploads import bp as uploads_bp
|
|
|
|
app.register_blueprint(overview_bp)
|
|
app.register_blueprint(seo_bp)
|
|
app.register_blueprint(security_bp)
|
|
app.register_blueprint(uploads_bp)
|
|
app.register_blueprint(api_bp)
|
|
app.register_blueprint(auth_bp)
|
|
|
|
|
|
def register_cli(app: Flask) -> None:
|
|
"""Attach `flask <command>` admin processes (factor 12)."""
|
|
from app.cli import register_commands
|
|
|
|
register_commands(app)
|