From f195818f07f3c3102295e46b298c8f957deb12ab Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 11 Feb 2026 21:33:21 +0000 Subject: [PATCH] Upgrade fastapi-vue-setup 1.0.2 --- paskia/fastapi/__main__.py | 61 +++++++++---------------------- scripts/devserver.py | 75 ++++++++++++++++---------------------- 2 files changed, 50 insertions(+), 86 deletions(-) diff --git a/paskia/fastapi/__main__.py b/paskia/fastapi/__main__.py index 3443109..4b9d4a4 100644 --- a/paskia/fastapi/__main__.py +++ b/paskia/fastapi/__main__.py @@ -5,23 +5,20 @@ import logging import os from urllib.parse import urlparse +from fastapi_vue import server from fastapi_vue.hostutil import parse_endpoint -from uvicorn import Config as UvicornConfig -from uvicorn import Server -from uvicorn import run as uvicorn_run from paskia import db from paskia import globals as _globals from paskia.bootstrap import bootstrap_if_needed from paskia.config import PaskiaConfig -from paskia.db import init as db_init from paskia.db.background import flush from paskia.db.structs import Config from paskia.util import startupbox from paskia.util.hostutil import normalize_origin DEFAULT_PORT = 4401 -DEVMODE = bool(os.getenv("PASKIA_FRONTEND_URL")) +DEVMODE = os.getenv("PASKIA_DEV") == "1" EPILOG = """\ Example: @@ -87,6 +84,7 @@ def main(): parser.add_argument( "-l", "--listen", + action="append", metavar="LISTEN", help=( "Endpoint to listen on (default: localhost:4401). " @@ -106,7 +104,7 @@ def main(): args.listen = None # Init db and load stored config - asyncio.run(db_init(rp_id=args.rp_id)) + asyncio.run(db.init(rp_id=args.rp_id)) stored_config = db.data().config # Apply defaults from stored config @@ -119,8 +117,9 @@ def main(): if args.listen is None and stored_config.listen is not None: args.listen = stored_config.listen - # Parse endpoint using fastapi_vue.hostutil - endpoints = parse_endpoint(args.listen, DEFAULT_PORT) + # Parse first endpoint for config display and site_url + first_listen = args.listen[0] if isinstance(args.listen, list) else args.listen + endpoints = parse_endpoint(first_listen, DEFAULT_PORT) # Extract host/port/uds from first endpoint for config display and site_url ep = endpoints[0] if endpoints else {} @@ -208,20 +207,7 @@ def main(): listen=args.listen, ) - run_kwargs: dict = { - "log_level": "warning", # Suppress startup messages; we use custom logging - "access_log": False, # We use custom AccessLogMiddleware instead - } - - if DEVMODE: - # Security: dev mode must run on localhost:4402 to prevent - # accidental public exposure of the Vite dev server - if host != "localhost" or port != 4402: - raise SystemExit(f"Dev mode requires localhost:4402, got {host}:{port}") - run_kwargs["reload"] = True - run_kwargs["reload_dirs"] = ["paskia"] - - async def async_main(): + async def startup(): await _globals.init( rp_id=config.rp_id, rp_name=config.rp_name, @@ -235,28 +221,17 @@ def main(): await db.update_config(cli_config) await flush() - if len(endpoints) > 1: - async with asyncio.TaskGroup() as tg: - for ep in endpoints: - tg.create_task( - Server( - UvicornConfig(app="paskia.fastapi:app", **run_kwargs, **ep) - ).serve() - ) - elif DEVMODE: - # Use uvicorn.run for proper reload support (it handles subprocess spawning) - ep = endpoints[0] - uvicorn_run("paskia.fastapi:app", **run_kwargs, **ep) - else: - server = Server( - UvicornConfig(app="paskia.fastapi:app", **run_kwargs, **endpoints[0]) - ) - await server.serve() + asyncio.run(startup()) - try: - asyncio.run(async_main()) - except KeyboardInterrupt: - pass + dev = {"reload": True, "reload_dirs": ["paskia"]} if DEVMODE else {} + server.run( + "paskia.fastapi.mainapp:app", + listen=args.listen, + default_port=DEFAULT_PORT, + log_level="warning", + access_log=False, + **dev, + ) if __name__ == "__main__": diff --git a/scripts/devserver.py b/scripts/devserver.py index 75582cb..8405f3a 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -1,28 +1,5 @@ #!/usr/bin/env -S uv run -"""Run Vite development server for frontend and Paskia backend with auto-reload. - -This script is only available when running from the git repository source, -not from the installed package. It starts both the Vite frontend dev server -and the Paskia backend with auto-reload enabled. - -Usage: - uv run scripts/devserver.py [-l host:port] [options...] - -The optional -l/--listen argument sets where the Vite frontend listens. -All other options are forwarded to `paskia`. -Backend always listens on localhost:4402. - -Environment: - PASKIA_FRONTEND_URL Set by this script for the backend to know where Vite is. - PASKIA_BACKEND_URL Set by this script for Vite to know where to proxy API calls. - PASKIA_SITE_URL User-facing URL for reset links (Caddy HTTPS or Vite HTTP). - -Options: - --caddy Run Caddy as HTTPS proxy on port 443 (requires sudo) - --rp-id HOST Relying Party ID (used as hostname for Caddy) - --origin URL Allowed origin(s), passed to backend - --auth-host H Dedicated auth host, passed to backend -""" +"""Run Vite development server for Vue app and FastAPI backend with auto-reload.""" import argparse import asyncio @@ -45,26 +22,26 @@ from devutil import ( # noqa: E402 setup_vite, ) -DEFAULT_VITE_PORT = 4403 # overrides by CLI option -BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts +DEFAULT_VITE_PORT = 4403 +DEFAULT_DEV_PORT = 4402 CADDY_PORT = 443 # HTTPS port for Caddy proxy CADDY_HTTP_PORT = 80 # HTTP port for ACME challenges CADDYFILE_SITE_BLOCK = """\ SITE_ADDR { - # WebSockets bypass directly to backend (workaround for bun proxy bug) + # WebSockets bypass directly to backend (workaround for bun proxy bug) handle /auth/ws/* { - reverse_proxy localhost:BACKEND_PORT + reverse_proxy BACKEND_ADDR } # Everything else goes to or via Vite handle { - reverse_proxy localhost:VITE_PORT + reverse_proxy VITE_ADDR } } """ -def build_caddyfile(origins: list[str], vite_port: int) -> str: +def build_caddyfile(origins: list[str], viteurl: str, backurl: str) -> str: """Build a Caddyfile for the given origins.""" caddyfile_parts = [] for origin in origins: @@ -78,21 +55,23 @@ def build_caddyfile(origins: list[str], vite_port: int) -> str: site_addr = f"{scheme}://{host}:{port}" block = ( CADDYFILE_SITE_BLOCK.replace("SITE_ADDR", site_addr) - .replace("BACKEND_PORT", str(BACKEND_PORT)) - .replace("VITE_PORT", str(vite_port)) + .replace("BACKEND_ADDR", backurl) + .replace("VITE_ADDR", viteurl) ) caddyfile_parts.append(block) return "\n".join(caddyfile_parts) -async def run_caddy(origins: list[str], vite_port: int) -> asyncio.subprocess.Process: +async def run_caddy( + origins: list[str], viteurl: str, backurl: str +) -> asyncio.subprocess.Process: """Start Caddy as HTTPS reverse proxy, wait for ready signal.""" caddy_path = shutil.which("caddy") if not caddy_path: logger.warning("Caddy not found. Install it to use --caddy option.") raise SystemExit(1) - caddyfile = build_caddyfile(origins, vite_port) + caddyfile = build_caddyfile(origins, viteurl, backurl) cmd = ["sudo", caddy_path, "run", "--config", "-", "--adapter", "caddyfile"] logger.info(">>> sudo caddy @ %s", " ".join(origins)) @@ -163,10 +142,7 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None: raise SystemExit(1) viteurl, npm_install, vite = setup_vite(args.listen, DEFAULT_VITE_PORT) - backurl, paskia = setup_cli("paskia", f"localhost:{BACKEND_PORT}", BACKEND_PORT) - - # Extract vite port for Caddy config - vite_port = int(viteurl.rsplit(":", 1)[1]) + backurl, paskia = setup_cli("paskia", args.backend, DEFAULT_DEV_PORT) # Build paskia command with options paskia.extend(["--rp-id", args.rp_id]) @@ -197,16 +173,17 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None: caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))] # Set environment for subprocesses - os.environ["PASKIA_FRONTEND_URL"] = viteurl + os.environ["PASKIA_VITE_URL"] = viteurl os.environ["PASKIA_BACKEND_URL"] = backurl os.environ["PASKIA_SITE_URL"] = caddy_origins[0] if args.caddy else viteurl + os.environ["PASKIA_DEV"] = "1" if args.auth_host: os.environ["PASKIA_AUTH_HOST"] = args.auth_host async with ProcessGroup() as pg: # Start Caddy first if requested (needs to bind ports) if args.caddy: - caddy_proc = await run_caddy(caddy_origins, vite_port) + caddy_proc = await run_caddy(caddy_origins, viteurl, backurl) pg._procs.append(caddy_proc) pg._cmds[caddy_proc.pid] = "caddy" @@ -222,9 +199,13 @@ def main(): parser.add_argument( "-l", "--listen", - metavar="ENDPOINT", - default=None, - help="Vite frontend endpoint (default: localhost:4403)", + metavar="addr", + help=f"Vite (default: localhost:{DEFAULT_VITE_PORT})", + ) + parser.add_argument( + "--backend", + metavar="addr", + help=f"FastAPI (default: localhost:{DEFAULT_DEV_PORT})", ) parser.add_argument("--caddy", action="store_true", help="Run Caddy as HTTPS proxy") parser.add_argument("--rp-id", default="localhost", help="Relying Party ID") @@ -238,5 +219,13 @@ def main(): asyncio.run(run_devserver(args, remaining)) +HELP_EPILOG = """ + Other options are forwarded to paskia [args] + + JS_RUNTIME environment variable can be used to select the JS runtime: + npm, deno, bun, or full path to the runtime executable (node maps to npm). +""" + + if __name__ == "__main__": main()