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 import os
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi_vue import server
from fastapi_vue.hostutil import parse_endpoint 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 db
from paskia import globals as _globals from paskia import globals as _globals
from paskia.bootstrap import bootstrap_if_needed from paskia.bootstrap import bootstrap_if_needed
from paskia.config import PaskiaConfig from paskia.config import PaskiaConfig
from paskia.db import init as db_init
from paskia.db.background import flush from paskia.db.background import flush
from paskia.db.structs import Config from paskia.db.structs import Config
from paskia.util import startupbox from paskia.util import startupbox
from paskia.util.hostutil import normalize_origin from paskia.util.hostutil import normalize_origin
DEFAULT_PORT = 4401 DEFAULT_PORT = 4401
DEVMODE = bool(os.getenv("PASKIA_FRONTEND_URL")) DEVMODE = os.getenv("PASKIA_DEV") == "1"
EPILOG = """\ EPILOG = """\
Example: Example:
@@ -87,6 +84,7 @@ def main():
parser.add_argument( parser.add_argument(
"-l", "-l",
"--listen", "--listen",
action="append",
metavar="LISTEN", metavar="LISTEN",
help=( help=(
"Endpoint to listen on (default: localhost:4401). " "Endpoint to listen on (default: localhost:4401). "
@@ -106,7 +104,7 @@ def main():
args.listen = None args.listen = None
# Init db and load stored config # 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 stored_config = db.data().config
# Apply defaults from stored config # Apply defaults from stored config
@@ -119,8 +117,9 @@ def main():
if args.listen is None and stored_config.listen is not None: if args.listen is None and stored_config.listen is not None:
args.listen = stored_config.listen args.listen = stored_config.listen
# Parse endpoint using fastapi_vue.hostutil # Parse first endpoint for config display and site_url
endpoints = parse_endpoint(args.listen, DEFAULT_PORT) 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 # Extract host/port/uds from first endpoint for config display and site_url
ep = endpoints[0] if endpoints else {} ep = endpoints[0] if endpoints else {}
@@ -208,20 +207,7 @@ def main():
listen=args.listen, listen=args.listen,
) )
run_kwargs: dict = { async def startup():
"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():
await _globals.init( await _globals.init(
rp_id=config.rp_id, rp_id=config.rp_id,
rp_name=config.rp_name, rp_name=config.rp_name,
@@ -235,28 +221,17 @@ def main():
await db.update_config(cli_config) await db.update_config(cli_config)
await flush() await flush()
if len(endpoints) > 1: asyncio.run(startup())
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()
try: dev = {"reload": True, "reload_dirs": ["paskia"]} if DEVMODE else {}
asyncio.run(async_main()) server.run(
except KeyboardInterrupt: "paskia.fastapi.mainapp:app",
pass listen=args.listen,
default_port=DEFAULT_PORT,
log_level="warning",
access_log=False,
**dev,
)
if __name__ == "__main__": if __name__ == "__main__":
+32 -43
View File
@@ -1,28 +1,5 @@
#!/usr/bin/env -S uv run #!/usr/bin/env -S uv run
"""Run Vite development server for frontend and Paskia backend with auto-reload. """Run Vite development server for Vue app and FastAPI 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
"""
import argparse import argparse
import asyncio import asyncio
@@ -45,26 +22,26 @@ from devutil import ( # noqa: E402
setup_vite, setup_vite,
) )
DEFAULT_VITE_PORT = 4403 # overrides by CLI option DEFAULT_VITE_PORT = 4403
BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts DEFAULT_DEV_PORT = 4402
CADDY_PORT = 443 # HTTPS port for Caddy proxy CADDY_PORT = 443 # HTTPS port for Caddy proxy
CADDY_HTTP_PORT = 80 # HTTP port for ACME challenges CADDY_HTTP_PORT = 80 # HTTP port for ACME challenges
CADDYFILE_SITE_BLOCK = """\ CADDYFILE_SITE_BLOCK = """\
SITE_ADDR { 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/* { handle /auth/ws/* {
reverse_proxy localhost:BACKEND_PORT reverse_proxy BACKEND_ADDR
} }
# Everything else goes to or via Vite # Everything else goes to or via Vite
handle { 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.""" """Build a Caddyfile for the given origins."""
caddyfile_parts = [] caddyfile_parts = []
for origin in origins: for origin in origins:
@@ -78,21 +55,23 @@ def build_caddyfile(origins: list[str], vite_port: int) -> str:
site_addr = f"{scheme}://{host}:{port}" site_addr = f"{scheme}://{host}:{port}"
block = ( block = (
CADDYFILE_SITE_BLOCK.replace("SITE_ADDR", site_addr) CADDYFILE_SITE_BLOCK.replace("SITE_ADDR", site_addr)
.replace("BACKEND_PORT", str(BACKEND_PORT)) .replace("BACKEND_ADDR", backurl)
.replace("VITE_PORT", str(vite_port)) .replace("VITE_ADDR", viteurl)
) )
caddyfile_parts.append(block) caddyfile_parts.append(block)
return "\n".join(caddyfile_parts) 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.""" """Start Caddy as HTTPS reverse proxy, wait for ready signal."""
caddy_path = shutil.which("caddy") caddy_path = shutil.which("caddy")
if not caddy_path: if not caddy_path:
logger.warning("Caddy not found. Install it to use --caddy option.") logger.warning("Caddy not found. Install it to use --caddy option.")
raise SystemExit(1) raise SystemExit(1)
caddyfile = build_caddyfile(origins, vite_port) caddyfile = build_caddyfile(origins, viteurl, backurl)
cmd = ["sudo", caddy_path, "run", "--config", "-", "--adapter", "caddyfile"] cmd = ["sudo", caddy_path, "run", "--config", "-", "--adapter", "caddyfile"]
logger.info(">>> sudo caddy @ %s", " ".join(origins)) 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) raise SystemExit(1)
viteurl, npm_install, vite = setup_vite(args.listen, DEFAULT_VITE_PORT) viteurl, npm_install, vite = setup_vite(args.listen, DEFAULT_VITE_PORT)
backurl, paskia = setup_cli("paskia", f"localhost:{BACKEND_PORT}", BACKEND_PORT) backurl, paskia = setup_cli("paskia", args.backend, DEFAULT_DEV_PORT)
# Extract vite port for Caddy config
vite_port = int(viteurl.rsplit(":", 1)[1])
# Build paskia command with options # Build paskia command with options
paskia.extend(["--rp-id", args.rp_id]) 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))] caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))]
# Set environment for subprocesses # 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_BACKEND_URL"] = backurl
os.environ["PASKIA_SITE_URL"] = caddy_origins[0] if args.caddy else viteurl os.environ["PASKIA_SITE_URL"] = caddy_origins[0] if args.caddy else viteurl
os.environ["PASKIA_DEV"] = "1"
if args.auth_host: if args.auth_host:
os.environ["PASKIA_AUTH_HOST"] = args.auth_host os.environ["PASKIA_AUTH_HOST"] = args.auth_host
async with ProcessGroup() as pg: async with ProcessGroup() as pg:
# Start Caddy first if requested (needs to bind ports) # Start Caddy first if requested (needs to bind ports)
if args.caddy: 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._procs.append(caddy_proc)
pg._cmds[caddy_proc.pid] = "caddy" pg._cmds[caddy_proc.pid] = "caddy"
@@ -222,9 +199,13 @@ def main():
parser.add_argument( parser.add_argument(
"-l", "-l",
"--listen", "--listen",
metavar="ENDPOINT", metavar="addr",
default=None, help=f"Vite (default: localhost:{DEFAULT_VITE_PORT})",
help="Vite frontend endpoint (default: localhost:4403)", )
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("--caddy", action="store_true", help="Run Caddy as HTTPS proxy")
parser.add_argument("--rp-id", default="localhost", help="Relying Party ID") parser.add_argument("--rp-id", default="localhost", help="Relying Party ID")
@@ -238,5 +219,13 @@ def main():
asyncio.run(run_devserver(args, remaining)) 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__": if __name__ == "__main__":
main() main()