Upgrade fastapi-vue-setup 1.0.2

This commit is contained in:
2026-02-11 21:33:21 +00:00
parent 97064f7bc7
commit ef7a6c8011
2 changed files with 50 additions and 86 deletions
+18 -43
View File
@@ -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__":
+32 -43
View File
@@ -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()