3394be4ee9
When --oidc-issuer is set, validate tokens as RS256 id_tokens fetched against bincio-auth's JWKS endpoint (cached for 1h). Falls back to HS256 if the RS256 check fails, so existing sessions keep working during the transition. DB session lookup is the final fallback. New --oidc-issuer flag reads BINCIO_OIDC_ISSUER env var.
112 lines
5.4 KiB
Python
112 lines
5.4 KiB
Python
"""bincio serve — CLI entry point for the multi-user VPS server."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from rich.console import Console
|
|
|
|
console = Console()
|
|
|
|
|
|
@click.command("serve")
|
|
@click.option("--data-dir", required=True, type=click.Path(), help="BAS data directory (contains instance.db)")
|
|
@click.option("--site-dir", default=None, type=click.Path(), help="Astro site dir for post-write rebuilds")
|
|
@click.option("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1 — proxy via nginx)")
|
|
@click.option("--port", default=4041, help="Bind port (default: 4041)")
|
|
@click.option("--strava-client-id", default=None, envvar="STRAVA_CLIENT_ID", help="Strava OAuth client ID (enables per-user Strava sync)")
|
|
@click.option("--strava-client-secret", default=None, envvar="STRAVA_CLIENT_SECRET", help="Strava OAuth client secret")
|
|
@click.option("--max-users", default=None, type=int, help="Override max users for this instance (0 = unlimited; updates the DB setting)")
|
|
@click.option("--public-url", default=None, envvar="PUBLIC_URL", help="Public base URL (e.g. https://yourdomain.com). Required for Strava OAuth to work behind a reverse proxy.")
|
|
@click.option("--webroot", default=None, type=click.Path(), help="Nginx webroot (e.g. /var/www/bincio). When set, uploads trigger a full Astro build + rsync so new activity pages are immediately accessible without a git push.")
|
|
@click.option("--dem-url", default=None, envvar="DEM_URL", help="Base URL of an Open-Elevation-compatible API (default: https://api.open-elevation.com).")
|
|
@click.option("--sync-secret", default=None, envvar="BINCIO_SYNC_SECRET", help="Shared secret for POST /api/internal/rebuild (used by the sync-strava systemd timer).")
|
|
@click.option("--jwt-secret", default=None, envvar="BINCIO_AUTH_JWT_SECRET", help="Shared JWT secret from bincio-auth. When set, validates JWTs locally instead of DB session lookup.")
|
|
@click.option("--auth-api", default=None, envvar="BINCIO_AUTH_API", help="Internal URL of the bincio-auth API (e.g. http://127.0.0.1:4040). When set, admin user-state operations are proxied to bincio-auth.")
|
|
@click.option("--oidc-issuer", default=None, envvar="BINCIO_OIDC_ISSUER", help="OIDC issuer URL (e.g. https://bincio.org). When set, validates RS256 id_tokens via JWKS (preferred over HS256).")
|
|
def serve(data_dir: str, site_dir: str | None, host: str, port: int,
|
|
strava_client_id: str | None, strava_client_secret: str | None,
|
|
max_users: int | None, public_url: str | None,
|
|
webroot: str | None, dem_url: str | None,
|
|
sync_secret: str | None, jwt_secret: str | None, auth_api: str | None,
|
|
oidc_issuer: str | None) -> None:
|
|
"""Start the bincio multi-user application server.
|
|
|
|
Handles auth, user management, and write operations.
|
|
Intended to run behind nginx which serves static files.
|
|
|
|
Requires a data directory initialised with `bincio init`.
|
|
"""
|
|
import uvicorn
|
|
|
|
import bincio.serve.server as srv
|
|
from bincio.serve import deps
|
|
from bincio.serve.db import get_setting, open_db, set_setting
|
|
|
|
dd = Path(data_dir).expanduser().resolve()
|
|
if not (dd / "instance.db").exists():
|
|
raise click.UsageError(
|
|
f"No instance.db found in {dd}. Run `bincio init --data-dir {dd}` first."
|
|
)
|
|
|
|
if max_users is not None:
|
|
db = open_db(dd)
|
|
set_setting(db, "max_users", str(max_users))
|
|
db.close()
|
|
|
|
deps.data_dir = dd
|
|
if site_dir:
|
|
deps.site_dir = Path(site_dir).expanduser().resolve()
|
|
if strava_client_id:
|
|
deps.strava_client_id = strava_client_id
|
|
if strava_client_secret:
|
|
deps.strava_client_secret = strava_client_secret
|
|
if public_url:
|
|
deps.public_url = public_url
|
|
if webroot and site_dir:
|
|
deps.webroot = Path(webroot).expanduser().resolve()
|
|
if dem_url:
|
|
deps.dem_url = dem_url
|
|
if sync_secret:
|
|
deps.sync_secret = sync_secret
|
|
if jwt_secret:
|
|
deps.jwt_secret = jwt_secret
|
|
if auth_api:
|
|
deps.auth_api = auth_api.rstrip("/")
|
|
if oidc_issuer:
|
|
deps.oidc_issuer = oidc_issuer
|
|
|
|
db = open_db(dd)
|
|
current_limit = get_setting(db, "max_users")
|
|
db.close()
|
|
|
|
console.print("[bold]bincio serve[/bold]")
|
|
console.print(f" Data: [cyan]{dd}[/cyan]")
|
|
if deps.site_dir:
|
|
console.print(f" Site: [cyan]{deps.site_dir}[/cyan]")
|
|
if deps.webroot:
|
|
console.print(f" Web: [cyan]{deps.webroot}[/cyan] (auto-rebuild on upload)")
|
|
console.print(f" URL: [cyan]http://{host}:{port}[/cyan]")
|
|
if current_limit and int(current_limit) > 0:
|
|
console.print(f" Users: [yellow]max {current_limit}[/yellow]")
|
|
else:
|
|
console.print(" Users: [dim]unlimited[/dim]")
|
|
console.print(f" DEM: [cyan]{deps.dem_url}[/cyan]")
|
|
if deps.oidc_issuer:
|
|
console.print(f" Auth: [green]RS256 via {deps.oidc_issuer}[/green]" + (" + HS256 fallback" if deps.jwt_secret else ""))
|
|
elif deps.jwt_secret:
|
|
console.print(" Auth: [green]JWT HS256 (bincio-auth)[/green]")
|
|
else:
|
|
console.print(" Auth: [dim]local DB sessions[/dim]")
|
|
console.print()
|
|
|
|
log_config = uvicorn.config.LOGGING_CONFIG.copy()
|
|
# Make bincio.serve logger emit at INFO through uvicorn's handler
|
|
log_config["loggers"]["bincio.serve"] = {
|
|
"handlers": ["default"],
|
|
"level": "INFO",
|
|
"propagate": False,
|
|
}
|
|
uvicorn.run(srv.app, host=host, port=port, log_level="info", log_config=log_config)
|