diff --git a/paskia/__main__.py b/paskia/__main__.py index 4b9d4a4..60a9a46 100644 --- a/paskia/__main__.py +++ b/paskia/__main__.py @@ -6,7 +6,7 @@ import os from urllib.parse import urlparse from fastapi_vue import server -from fastapi_vue.hostutil import parse_endpoint +from fastapi_vue.hostutil import parse_endpoints from paskia import db from paskia import globals as _globals @@ -56,6 +56,7 @@ def add_common_options(p: argparse.ArgumentParser) -> None: "--origin", action="append", dest="origins", + default=[], metavar="URL", help="Allowed origin URL(s). May be specified multiple times. If any are specified, only those origins are permitted for WebSocket authentication.", ) @@ -118,42 +119,25 @@ def main(): args.listen = stored_config.listen # 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) + ep = next(iter(parse_endpoints(args.listen, DEFAULT_PORT)), {}) + host, port, uds = ep.get("host"), ep.get("port"), ep.get("uds") - # Extract host/port/uds from first endpoint for config display and site_url - ep = endpoints[0] if endpoints else {} - host = ep.get("host") - port = ep.get("port") - uds = ep.get("uds") - - # Collect and normalize origins, handle auth_host - origins = [normalize_origin(o) for o in (getattr(args, "origins", None) or [])] + # Process and normalize auth_host if args.auth_host: - # Normalize auth_host with scheme if "://" not in args.auth_host: args.auth_host = f"https://{args.auth_host}" - + args.auth_host = args.auth_host.rstrip("/") validate_auth_host(args.auth_host, args.rp_id) + args.origins.insert(0, args.auth_host) # Ensure first in origins - # If origins are configured, ensure auth_host is included at top - if origins: - # Insert auth_host at the beginning - origins.insert(0, args.auth_host) - - # Remove duplicates while preserving order - seen = set() - origins = [x for x in origins if not (x in seen or seen.add(x))] + # Normalize, strip trailing slashes, and deduplicate while preserving order + origins = list({normalize_origin(o).rstrip("/"): ... for o in (args.origins)}) # Compute site_url and site_path for reset links - # Priority: PASKIA_SITE_URL (explicit) > auth_host > first origin with localhost > http://localhost:port - explicit_site_url = os.environ.get("PASKIA_SITE_URL") - if explicit_site_url: - # Explicit site URL from devserver or deployment config - site_url = explicit_site_url.rstrip("/") - site_path = "/" if args.auth_host else "/auth/" - elif args.auth_host: - site_url = args.auth_host.rstrip("/") + # Priority: auth_host > first configured origin > PASKIA_VITE_URL (devserver) > http://localhost:port > https://rp_id + site_path = "/auth/" + if args.auth_host: + site_url = args.auth_host site_path = "/" elif origins: # Find localhost origin if rp_id is localhost, else use first origin @@ -162,15 +146,13 @@ def main(): if args.rp_id == "localhost" else None ) - site_url = (localhost_origin or origins[0]).rstrip("/") - site_path = "/auth/" + site_url = localhost_origin or origins[0] + elif vite_url := os.environ.get("PASKIA_VITE_URL"): + site_url = vite_url.rstrip("/") # Devserver elif args.rp_id == "localhost" and port: - # Dev mode: use http with port - site_url = f"http://localhost:{port}" - site_path = "/auth/" + site_url = f"http://localhost:{port}" # Backend directly if we can else: - site_url = f"https://{args.rp_id}" - site_path = "/auth/" + site_url = f"https://{args.rp_id}" # Assume external reverse proxy # Build runtime configuration config = PaskiaConfig( diff --git a/pyproject.toml b/pyproject.toml index 424ef29..36a8f70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,20 +11,29 @@ keywords = [ "forward_auth", "auth_request", "FastAPI" ] authors = [ {name = "Leo Vasanko"}, ] +requires-python = ">=3.11" dependencies = [ - "fastapi[standard]>=0.104.1", - "websockets>=12.0", - "webauthn>=1.11.1", - "base64url>=1.0.0", - "uuid7-standard>=1.0.0", - "pyjwt[crypto]>=2.8.0", + "fastapi[standard]>=0.129.0", + "websockets>=16.0", + "webauthn>=2.7.1", + "base64url>=1.1.1", + "uuid7-standard>=1.1.0", + "pyjwt[crypto]>=2.11.0", "jsondiff>=2.2.1", "msgspec>=0.20.0", "aiofiles>=25.1.0", - "fastapi-vue>=0.3.0", + "fastapi-vue>=1.1.0", "ua-parser[regex]>=1.0.1", ] -requires-python = ">=3.11" +[dependency-groups] +dev = [ + "coverage>=7.13.4", + "httpx>=0.28.1", + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", + "pytest-cov>=7.0.0", + "ruff>=0.15.1", +] [project.urls] Homepage = "https://git.zi.fi/LeoVasanko/paskia" @@ -36,15 +45,6 @@ source = "vcs" [tool.hatch.build.hooks.vcs] version-file = "paskia/_version.py" -[project.optional-dependencies] -dev = [ - "ruff>=0.1.0", - "coverage[toml]>=7.0.0", - "pytest>=8.0.0", - "pytest-asyncio>=0.24.0", - "httpx>=0.27.0", -] - [tool.coverage.run] source = ["paskia"] branch = true @@ -75,16 +75,6 @@ select = ["E", "F", "I", "N", "W", "UP", "PLC0415"] ignore = ["E501"] # Line too long isort.known-first-party = ["paskia"] -[dependency-groups] -dev = [ - "coverage>=7.12.0", - "httpx>=0.28.1", - "pytest>=9.0.1", - "pytest-asyncio>=1.3.0", - "pytest-cov>=7.0.0", - "ruff>=0.14.8", -] - [project.scripts] paskia = "paskia.__main__:main" diff --git a/scripts/devserver.py b/scripts/devserver.py index 8405f3a..55594b1 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -153,29 +153,9 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None: paskia.extend(["--origin", origin]) paskia.extend(remaining) - # Compute origins for Caddy - caddy_origins = [] - if args.auth_host: - auth_host = args.auth_host - if "://" not in auth_host: - auth_host = f"https://{auth_host}" - caddy_origins.append(auth_host) - caddy_origins.append(f"https://{args.rp_id}") - if args.origins: - for origin in args.origins: - if "://" not in origin: - origin = f"https://{origin}" - caddy_origins.append(origin) - if not args.auth_host and not args.origins: - caddy_origins.append(f"https://{args.rp_id}") - # Remove duplicates while preserving order - seen = set() - caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))] - # Set environment for subprocesses 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 @@ -183,6 +163,22 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None: async with ProcessGroup() as pg: # Start Caddy first if requested (needs to bind ports) if args.caddy: + caddy_origins = [] + if args.auth_host: + auth_host = args.auth_host + if "://" not in auth_host: + auth_host = f"https://{auth_host}" + caddy_origins.append(auth_host) + caddy_origins.append(f"https://{args.rp_id}") + if args.origins: + for origin in args.origins: + if "://" not in origin: + origin = f"https://{origin}" + caddy_origins.append(origin) + if not caddy_origins: + caddy_origins.append(f"https://{args.rp_id}") + seen: set = set() + caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))] caddy_proc = await run_caddy(caddy_origins, viteurl, backurl) pg._procs.append(caddy_proc) pg._cmds[caddy_proc.pid] = "caddy"