from __future__ import annotations import mimetypes import os import secrets from datetime import timedelta from quart import Quart, jsonify, send_from_directory from . import __version__ from .auth import bp as auth_bp from .config import Config from .db import session_scope from .graph import bp as graph_bp from .labels import bp as labels_bp from .notes import bp as notes_bp from .settings import get_public_config, get_setting, load_or_create_secret_key from .settings_api import bp as settings_bp STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") # `.webmanifest` isn't in every base image's mime map; register it so the PWA # manifest is served as application/manifest+json instead of octet-stream. mimetypes.add_type("application/manifest+json", ".webmanifest") def create_app() -> Quart: # static_folder=None: the SPA catch-all below owns static serving. app = Quart(__name__, static_folder=None) # Ephemeral/env secret so the app (and DB-free unit tests) construct without a # database. before_serving swaps in the real, DB-persisted key before serving. app.secret_key = Config.secret_key_env() or secrets.token_urlsafe(48) app.config["APP_VERSION"] = os.environ.get("APP_VERSION", __version__) app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30) app.config["MAX_CONTENT_LENGTH"] = 12 * 1024 * 1024 # image-upload body cap app.register_blueprint(auth_bp) app.register_blueprint(notes_bp) app.register_blueprint(labels_bp) app.register_blueprint(graph_bp) app.register_blueprint(settings_bp) @app.before_serving async def _bootstrap() -> None: # Load (or generate + persist) the real signing secret and the live session # lifetime from the DB, before any request is served. async with session_scope() as db: app.secret_key = await load_or_create_secret_key(db) try: days = int(await get_setting(db, "session_ttl_days")) app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=days) except (ValueError, TypeError, KeyError): pass @app.get("/api/health") async def health(): return jsonify({"status": "ok", "version": app.config["APP_VERSION"]}) @app.get("/api/config") async def public_config(): # Public: the login/register screen reads site name + whether signups are open. async with session_scope() as db: data = await get_public_config(db) data["version"] = app.config["APP_VERSION"] return jsonify(data) @app.get("/", defaults={"path": ""}) @app.get("/") async def spa(path: str): if path.startswith("api/"): return jsonify({"error": "not found"}), 404 candidate = os.path.join(STATIC_DIR, path) if path and os.path.isfile(candidate): return await send_from_directory(STATIC_DIR, path) index = os.path.join(STATIC_DIR, "index.html") if os.path.isfile(index): return await send_from_directory(STATIC_DIR, "index.html") return jsonify({"error": "frontend not built"}), 404 return app