porchlight/docs/plans/2026-02-17-cli-module-design.md
2026-02-17 14:09:14 +01:00

2.6 KiB

CLI Module Design

Problem

pyproject.toml declares porchlight = "porchlight.cli:main" with typer>=0.15 as a dependency, but no cli.py exists. The app has no CLI entry point for administrative tasks -- operators must use the Python shell or raw SQL to create invite links or bootstrap initial users.

Decision

Create src/porchlight/cli.py with two commands: create-invite and initial-admin. Skip a serve command since Docker/uvicorn handles that already.

Commands

porchlight create-invite <username>

Generate a magic link registration URL for a new user.

Options:

  • --ttl SECONDS -- link expiration (default: from settings, 86400s)
  • --note TEXT -- optional note stored with the link

Behavior:

  • Opens SQLite DB via existing open_db() context manager
  • Creates MagicLinkService with SQLiteMagicLinkRepository
  • Calls service.create(username=username, note=note, created_by="cli")
  • Prints full URL: {issuer}/register/{token}
  • Reads OIDC_OP_ISSUER env var for base URL (required)
  • Reads OIDC_OP_SQLITE_PATH for DB path (default: data/oidc_op.db)

porchlight initial-admin <username>

Bootstrap the first admin user with a registration link.

Options:

  • --group TEXT -- groups to assign (default: ["admin", "users"]), repeatable

Behavior:

  • Opens SQLite DB
  • Checks if username already exists -- error if so
  • Generates unique userid via generate_unique_userid()
  • Creates user with specified groups
  • Creates a magic link so the admin can visit the URL to set up credentials
  • Prints full URL: {issuer}/register/{token}

Route change: /register/{token}

Modify register_magic_link to handle existing users:

  • Before creating the user, call user_repo.get_by_username(link.username)
  • If user exists: skip creation, log them in, redirect to /manage/credentials?setup=1
  • If user doesn't exist: create as before with groups=["users"]

This makes the registration route work for both fresh invites and admin-created users who need to set up credentials.

Implementation notes

  • Both commands use asyncio.run() to call async helpers
  • Config loaded via Settings() (pydantic-settings reads env vars)
  • No new dependencies required -- Typer is already declared
  • No schema changes needed

Files changed

File Change
src/porchlight/cli.py New -- Typer app with two commands
src/porchlight/authn/routes.py Modify register_magic_link for existing users
tests/test_cli.py New -- tests for both CLI commands
tests/test_auth_routes/test_register_magic_link.py Add test for existing user registration