124 lines
3.7 KiB
Python
124 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, render_template
|
|
|
|
from app.config import ProductionConfig, config_by_name
|
|
from app.extensions import csrf, db, login_manager, migrate
|
|
|
|
|
|
def create_app(config_name: str | None = None, config_overrides: dict | None = None) -> Flask:
|
|
try:
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
except ImportError:
|
|
pass
|
|
|
|
app = Flask(__name__, instance_relative_config=True)
|
|
Path(app.instance_path).mkdir(parents=True, exist_ok=True)
|
|
|
|
selected_config = config_name or os.getenv("APP_ENV", "development")
|
|
config_class = config_by_name.get(selected_config, config_by_name["development"])
|
|
app.config.from_object(config_class)
|
|
if config_overrides:
|
|
app.config.update(config_overrides)
|
|
if config_class is ProductionConfig:
|
|
validate_production_config(app)
|
|
|
|
configure_logging(app)
|
|
init_extensions(app)
|
|
register_blueprints(app)
|
|
register_error_handlers(app)
|
|
register_security_headers(app)
|
|
register_template_context(app)
|
|
|
|
from app.commands import register_commands
|
|
|
|
register_commands(app)
|
|
return app
|
|
|
|
|
|
def init_extensions(app: Flask) -> None:
|
|
from app.models import Account
|
|
|
|
db.init_app(app)
|
|
migrate.init_app(app, db)
|
|
csrf.init_app(app)
|
|
|
|
login_manager.login_view = "auth.login"
|
|
login_manager.login_message_category = "warning"
|
|
login_manager.init_app(app)
|
|
|
|
@login_manager.user_loader
|
|
def load_user(account_id: str) -> Account | None:
|
|
if not account_id.isdigit():
|
|
return None
|
|
return db.session.get(Account, int(account_id))
|
|
|
|
|
|
def register_blueprints(app: Flask) -> None:
|
|
from app.admin.routes import bp as admin_bp
|
|
from app.attendance.routes import bp as attendance_bp
|
|
from app.auth.routes import bp as auth_bp
|
|
from app.main.routes import bp as main_bp
|
|
from app.people.routes import bp as people_bp
|
|
from app.recognition.routes import bp as recognition_bp
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(main_bp)
|
|
app.register_blueprint(admin_bp)
|
|
app.register_blueprint(people_bp)
|
|
app.register_blueprint(attendance_bp)
|
|
app.register_blueprint(recognition_bp)
|
|
|
|
|
|
def register_error_handlers(app: Flask) -> None:
|
|
@app.errorhandler(403)
|
|
def forbidden(error):
|
|
return render_template("errors/403.html"), 403
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
return render_template("errors/404.html"), 404
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(error):
|
|
db.session.rollback()
|
|
app.logger.exception("Unhandled application error: %s", error)
|
|
return render_template("errors/500.html"), 500
|
|
|
|
|
|
def register_security_headers(app: Flask) -> None:
|
|
@app.after_request
|
|
def set_default_security_headers(response):
|
|
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
|
|
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
response.headers.setdefault("Permissions-Policy", "camera=(self), microphone=()")
|
|
return response
|
|
|
|
|
|
def register_template_context(app: Flask) -> None:
|
|
from datetime import datetime, timezone
|
|
|
|
from app.models import Role
|
|
|
|
@app.context_processor
|
|
def inject_globals():
|
|
return {"current_year": datetime.now(timezone.utc).year, "Role": Role}
|
|
|
|
|
|
def configure_logging(app: Flask) -> None:
|
|
if app.debug or app.testing:
|
|
return
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
def validate_production_config(app: Flask) -> None:
|
|
if app.config["SECRET_KEY"] in {"", "dev-change-me"}:
|
|
raise RuntimeError("SECRET_KEY must be set for production.")
|