Change CLI to use --listen rather than positional hostport argument. Fix listening on all interfaces (runs lifespan twice, which needs to be OK). Update README with various CLI changes and more.
This commit is contained in:
@@ -29,12 +29,12 @@ Single Sign-On (SSO): Users register once and authenticate across all applicatio
|
||||
Install [UV](https://docs.astral.sh/uv/getting-started/installation/) and run:
|
||||
|
||||
```fish
|
||||
uvx paskia serve --rp-id example.com
|
||||
uvx paskia --rp-id example.com
|
||||
```
|
||||
|
||||
On the first run it downloads the software and prints a registration link for the Admin. The server will start up on [localhost:4401](http://localhost:4401) *for authentication required*, serving for `*.example.com`. If you are going to be connecting `localhost` directly, for testing, leave out the rp-id.
|
||||
On the first run it downloads the software and prints a registration link for the Admin. The server starts on [localhost:4401](http://localhost:4401), serving authentication for `*.example.com`. For local testing, leave out `--rp-id`.
|
||||
|
||||
Otherwise you will need a web server such as [Caddy](https://caddyserver.com/) to serve HTTPS on your actual domain names and proxy requests to Paskia and your backend apps (see documentation below).
|
||||
For production you need a web server such as [Caddy](https://caddyserver.com/) to serve HTTPS on your actual domain names and proxy requests to Paskia and your backend apps (see documentation below).
|
||||
|
||||
For a permanent install of `paskia` CLI command, not needing `uvx`:
|
||||
|
||||
@@ -44,19 +44,20 @@ uv tool install paskia
|
||||
|
||||
## Configuration
|
||||
|
||||
There is no config file. Pass only the options on CLI:
|
||||
There is no config file. All settings are passed as CLI options:
|
||||
|
||||
```text
|
||||
paskia serve [options]
|
||||
paskia [options]
|
||||
paskia reset [user] # Generate passkey reset link
|
||||
```
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| Listen address | One of *host***:***port* (default all hosts, port 4401) or **unix:***path***/paskia.socket** (Unix socket) | **localhost:4401** |
|
||||
| --rp-id *domain* | Main/top domain | **localhost** |
|
||||
| --rp-name *"text"* | Name of your company or site | Same as rp-id |
|
||||
| --origin *url* | Explicitly list the domain names served | **https://**_rp-id_ |
|
||||
| --auth-host *domain* | Dedicated authentication site (e.g., **auth.example.com**) | **Unspecified:** we use **/auth/** on **every** site under rp-id.|
|
||||
| -l, --listen *endpoint* | Listen address: *host*:*port*, :*port* (all interfaces), or */path.sock* | **localhost:4401** |
|
||||
| --rp-id *domain* | Main/top domain for passkeys | **localhost** |
|
||||
| --rp-name *"text"* | Name shown during passkey registration | Same as rp-id |
|
||||
| --origin *url* | Restrict allowed origins for WebSocket auth (repeatable) | All under rp-id |
|
||||
| --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site |
|
||||
|
||||
## Further Documentation
|
||||
|
||||
|
||||
+8
-7
@@ -15,18 +15,18 @@ from paskia.util import hostutil, passphrase
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Shared log message template for admin reset links
|
||||
ADMIN_RESET_MESSAGE = """\
|
||||
%s
|
||||
|
||||
ADMIN_RESET_MESSAGE = """
|
||||
👤 Admin %s
|
||||
- Use this link to register a Passkey for the admin user!
|
||||
"""
|
||||
|
||||
|
||||
def _log_reset_link(message: str, passphrase: str) -> str:
|
||||
def _log_reset_link(passphrase: str, message: str | None = None) -> str:
|
||||
"""Log a reset link message and return the URL."""
|
||||
reset_link = hostutil.reset_link_url(passphrase)
|
||||
logger.info(ADMIN_RESET_MESSAGE, message, reset_link)
|
||||
if message:
|
||||
logger.info(message)
|
||||
logger.info(ADMIN_RESET_MESSAGE, reset_link)
|
||||
return reset_link
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ async def bootstrap_system() -> None:
|
||||
reset_passphrase = db.bootstrap()
|
||||
|
||||
# Log the reset link (this is separate from the transaction log)
|
||||
_log_reset_link("✅ Bootstrap completed!", reset_passphrase)
|
||||
_log_reset_link(reset_passphrase, "✅ Bootstrap completed!")
|
||||
|
||||
|
||||
async def check_admin_credentials() -> bool:
|
||||
@@ -72,6 +72,7 @@ async def check_admin_credentials() -> bool:
|
||||
|
||||
if not db.get_user_credential_ids(admin_user.uuid):
|
||||
# Admin exists but has no credentials, create reset link
|
||||
logger.info("⚠️ Admin user has no credentials!")
|
||||
|
||||
token = passphrase.generate()
|
||||
expiry = authsession.reset_expires()
|
||||
@@ -81,7 +82,7 @@ async def check_admin_credentials() -> bool:
|
||||
expiry=expiry,
|
||||
token_type="admin registration",
|
||||
)
|
||||
_log_reset_link("⚠️ Admin user has no credentials!", token)
|
||||
_log_reset_link(token)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -74,21 +74,18 @@ async def start_background():
|
||||
_logger.debug("Background task in different event loop, restarting")
|
||||
_background_task = None
|
||||
else:
|
||||
# Task is running in the same event loop - this is an error
|
||||
raise RuntimeError(
|
||||
"Background task is already running. "
|
||||
"start_background() must not be called multiple times in the same event loop."
|
||||
# Task is already running in same loop - idempotent, just return
|
||||
# This happens with dual IPv4+IPv6 endpoints sharing the same process
|
||||
_logger.debug(
|
||||
"Background task already running in same loop, skipping"
|
||||
)
|
||||
except RuntimeError:
|
||||
raise # Re-raise RuntimeError from above
|
||||
return
|
||||
except Exception as e:
|
||||
_logger.debug("Error checking background task loop: %s, restarting", e)
|
||||
_background_task = None
|
||||
|
||||
if _background_task is None:
|
||||
_background_task = asyncio.create_task(_background_loop())
|
||||
else:
|
||||
_logger.debug("Background task already running: %s", _background_task)
|
||||
|
||||
|
||||
async def stop_background():
|
||||
|
||||
+21
-13
@@ -21,10 +21,10 @@ DEFAULT_PORT = 4401
|
||||
|
||||
EPILOG = """\
|
||||
Examples:
|
||||
paskia # localhost:4401
|
||||
paskia :8080 # All interfaces, port 8080
|
||||
paskia unix:/tmp/paskia.sock
|
||||
paskia reset [user] # Generate passkey reset link
|
||||
paskia # localhost:4401
|
||||
paskia -l :8080 # All interfaces, port 8080
|
||||
paskia -l /tmp/paskia.sock # Unix socket
|
||||
paskia reset [user] # Generate passkey reset link
|
||||
"""
|
||||
|
||||
|
||||
@@ -81,32 +81,40 @@ def main():
|
||||
epilog=EPILOG,
|
||||
)
|
||||
|
||||
# Primary argument: either host:port or "reset" subcommand
|
||||
# Subcommand for reset
|
||||
parser.add_argument(
|
||||
"hostport",
|
||||
"command",
|
||||
nargs="?",
|
||||
help=(
|
||||
"Endpoint (default: localhost:4401). Forms: host[:port] | :port | "
|
||||
"[ipv6][:port] | ipv6 | unix:/path.sock | 'reset' for credential reset"
|
||||
),
|
||||
help="Command: 'reset' for credential reset, or omit to run server",
|
||||
)
|
||||
parser.add_argument(
|
||||
"reset_query",
|
||||
nargs="?",
|
||||
help="For 'reset' command: user UUID or substring of display name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--listen",
|
||||
metavar="LISTEN",
|
||||
help=(
|
||||
"Endpoint to listen on (default: localhost:4401). "
|
||||
"Forms: host:port port :port [ipv6]:port unix:path /path.sock"
|
||||
),
|
||||
)
|
||||
add_common_options(parser)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Detect "reset" subcommand (first positional is "reset")
|
||||
is_reset = args.hostport == "reset"
|
||||
# Detect "reset" subcommand
|
||||
is_reset = args.command == "reset"
|
||||
|
||||
if is_reset:
|
||||
endpoints = []
|
||||
else:
|
||||
if args.command is not None:
|
||||
raise SystemExit(f"Unknown command: {args.command}")
|
||||
# Parse endpoint using fastapi_vue.hostutil
|
||||
endpoints = parse_endpoint(args.hostport, DEFAULT_PORT)
|
||||
endpoints = parse_endpoint(args.listen, DEFAULT_PORT)
|
||||
|
||||
# Extract host/port/uds from first endpoint for config display and site_url
|
||||
ep = endpoints[0] if endpoints else {}
|
||||
|
||||
+13
-7
@@ -6,9 +6,9 @@ not from the installed package. It starts both the Vite frontend dev server
|
||||
and the FastAPI backend with auto-reload enabled.
|
||||
|
||||
Usage:
|
||||
uv run scripts/devserver.py [host:port] [options...]
|
||||
uv run scripts/devserver.py [-l host:port] [options...]
|
||||
|
||||
The optional host:port argument sets where the Vite frontend listens.
|
||||
The optional -l/--listen argument sets where the Vite frontend listens.
|
||||
All other options are forwarded to `paskia`.
|
||||
Backend always listens on localhost:4402.
|
||||
|
||||
@@ -376,9 +376,15 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None:
|
||||
|
||||
|
||||
def main():
|
||||
# Parse optional hostport argument for Vite frontend
|
||||
# Parse optional listen argument for Vite frontend
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("hostport", nargs="?", default=None)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--listen",
|
||||
metavar="ENDPOINT",
|
||||
default=None,
|
||||
help="Vite frontend endpoint (default: localhost:4403)",
|
||||
)
|
||||
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(
|
||||
@@ -389,7 +395,7 @@ def main():
|
||||
|
||||
# Parse Vite endpoint
|
||||
vite_host, vite_port, vite_uds, all_ifaces = parse_endpoint(
|
||||
args.hostport, DEFAULT_VITE_PORT
|
||||
args.listen, DEFAULT_VITE_PORT
|
||||
)
|
||||
|
||||
if vite_uds:
|
||||
@@ -446,8 +452,8 @@ def main():
|
||||
# Start Vite dev server
|
||||
run_vite(vite_url, vite_host, vite_port, env, args.auth_host)
|
||||
|
||||
# Build command with origin args (no serve subcommand, host:port is first arg)
|
||||
cmd = ["paskia", f"localhost:{BACKEND_PORT}"]
|
||||
# Build command with origin args
|
||||
cmd = ["paskia", "-l", f"localhost:{BACKEND_PORT}"]
|
||||
|
||||
# Pass through rp-id (always pass, has default)
|
||||
cmd.extend(["--rp-id", args.rp_id])
|
||||
|
||||
Reference in New Issue
Block a user