Upgrade fastapi-vue-setup 1.0.2
This commit is contained in:
+18
-43
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user