auth: add FastAPI service — models, deps, server, routers, CLI
Steps 3–7 of the migration plan: - models.py: Pydantic request/response types - deps.py: shared state, JWT-based auth helpers, rate limiting - server.py: FastAPI app with CORS + gzip - routers/auth.py: login, logout, /api/me, reset-password, register - routers/invites.py: GET/POST /api/invites - routers/admin.py: user listing, suspend/unsuspend, delete, access flags, reset-password-code - cli.py: `bincio-auth init` (creates DB + admin + JWT secret) and `bincio-auth serve` Cookie carries a signed JWT (HS256); consumers validate locally with shared secret.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""bincio-auth CLI entry point."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
@click.group()
|
||||
def main() -> None:
|
||||
"""bincio-auth — central authentication service."""
|
||||
|
||||
|
||||
@main.command("init")
|
||||
@click.option("--data-dir", required=True, type=click.Path(), help="Data directory to initialise")
|
||||
@click.option("--handle", required=True, help="Admin user handle")
|
||||
@click.option("--password", required=True, hide_input=True, confirmation_prompt=True)
|
||||
@click.option("--display-name", default="", help="Admin display name (defaults to handle)")
|
||||
@click.option("--max-users", default=0, type=int, help="Max users (0 = unlimited)")
|
||||
def init_cmd(data_dir: str, handle: str, password: str, display_name: str, max_users: int) -> None:
|
||||
"""Bootstrap a fresh bincio-auth instance."""
|
||||
from bincio.auth.db import create_invite, create_user, get_user, open_db, set_setting
|
||||
|
||||
dd = Path(data_dir).expanduser().resolve()
|
||||
dd.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
console.print(f"[bold]Initialising bincio-auth[/bold] at [cyan]{dd}[/cyan]")
|
||||
|
||||
db = open_db(dd)
|
||||
console.print(" [green]✓[/green] instance.db ready")
|
||||
|
||||
existing = get_user(db, handle)
|
||||
if existing:
|
||||
console.print(f" [yellow]·[/yellow] user '{handle}' already exists — skipping")
|
||||
else:
|
||||
create_user(db, handle, display_name or handle, password, is_admin=True)
|
||||
console.print(f" [green]✓[/green] admin '{handle}' created")
|
||||
|
||||
if max_users > 0:
|
||||
set_setting(db, "max_users", str(max_users))
|
||||
console.print(f" [green]✓[/green] user limit set to {max_users}")
|
||||
|
||||
# Generate JWT secret if not already stored
|
||||
from bincio.auth.db import get_setting
|
||||
if not get_setting(db, "jwt_secret"):
|
||||
jwt_secret = secrets.token_hex(32)
|
||||
set_setting(db, "jwt_secret", jwt_secret)
|
||||
console.print(" [green]✓[/green] JWT secret generated and stored")
|
||||
else:
|
||||
console.print(" [yellow]·[/yellow] JWT secret already set — skipping")
|
||||
|
||||
code = create_invite(db, handle)
|
||||
|
||||
console.print()
|
||||
console.print(Panel(
|
||||
f"[bold green]Instance ready![/bold green]\n\n"
|
||||
f"Admin: [cyan]{handle}[/cyan]\n"
|
||||
f"Data dir: [cyan]{dd}[/cyan]\n\n"
|
||||
f"First invite code:\n\n"
|
||||
f" [bold yellow]{code}[/bold yellow]\n\n"
|
||||
f"Share this with your first user:\n"
|
||||
f" /register/?code={code}",
|
||||
title="bincio-auth init",
|
||||
border_style="green",
|
||||
))
|
||||
|
||||
|
||||
@main.command("serve")
|
||||
@click.option("--data-dir", required=True, type=click.Path(), help="Data directory (contains instance.db)")
|
||||
@click.option("--host", default="127.0.0.1")
|
||||
@click.option("--port", default=4040, type=int)
|
||||
@click.option("--jwt-secret", default=None, envvar="BINCIO_AUTH_JWT_SECRET",
|
||||
help="HS256 signing secret. Defaults to the value stored in the DB during `init`.")
|
||||
def serve_cmd(data_dir: str, host: str, port: int, jwt_secret: str | None) -> None:
|
||||
"""Start the bincio-auth API server."""
|
||||
import uvicorn
|
||||
|
||||
import bincio.auth.server as srv
|
||||
from bincio.auth import deps
|
||||
from bincio.auth.db import get_setting, open_db
|
||||
|
||||
dd = Path(data_dir).expanduser().resolve()
|
||||
if not (dd / "instance.db").exists():
|
||||
raise click.UsageError(
|
||||
f"No instance.db in {dd}. Run `bincio-auth init --data-dir {dd}` first."
|
||||
)
|
||||
|
||||
db = open_db(dd)
|
||||
secret = jwt_secret or get_setting(db, "jwt_secret") or ""
|
||||
db.close()
|
||||
|
||||
if not secret:
|
||||
raise click.UsageError(
|
||||
"No JWT secret configured. Pass --jwt-secret or run `bincio-auth init` first."
|
||||
)
|
||||
|
||||
deps.data_dir = dd
|
||||
deps.jwt_secret = secret
|
||||
|
||||
console.print("[bold]bincio-auth[/bold]")
|
||||
console.print(f" Data: [cyan]{dd}[/cyan]")
|
||||
console.print(f" URL: [cyan]http://{host}:{port}[/cyan]")
|
||||
console.print()
|
||||
|
||||
uvicorn.run(srv.app, host=host, port=port, log_level="info")
|
||||
Reference in New Issue
Block a user