rustio-admin-cli 0.18.4

Command-line tools for rustio-admin: project scaffolding, migrations, user management.
# {{name}}

A `rustio-admin` project. Generated by `rustio startproject {{name}}`.

## Quickstart

```sh
# 1. Create the database
createdb {{name}}_dev

# 2. Configure DATABASE_URL (only needed if your local Postgres user/password differs)
cp .env.example .env
$EDITOR .env

# 3. Apply migrations and create the first administrator
cargo install rustio-admin-cli
rustio migrate apply
rustio user create --email admin@{{name}}.local --role administrator
# The CLI prompts twice for the password (echo-suppressed). The
# plaintext value never reaches argv, ps output, or shell history.
# Use --password '...' only inside CI or scripted bootstrap.

# 4. Boot the admin
cargo run
```

The admin lands at <http://127.0.0.1:8000/admin>. Sign in with the
account from step 3.

### Port already in use?

`src/main.rs` binds to `127.0.0.1:8000`. If that port is taken on
your machine, change the listen address in the file:

```rust
let addr = "127.0.0.1:8001".parse().expect("valid listen address");
```

Pick any free port; nothing else in the framework cares about the
exact value. Re-run `cargo run`.

## What you get after first login

- Session-backed admin authentication (Argon2id passwords; cookies
  carry random tokens, the database stores SHA-256 hashes only).
- Postgres-backed users, groups, and per-model permissions (5-tier
  role ladder: User · Staff · Supervisor · Administrator · Developer).
- Permission matrix UI on the group edit page (model × action grid
  with a per-row "All" toggle).
- Audit history at `/admin/history` — every authority mutation
  recorded with actor, target, IP, correlation id.
- Active sessions page at `/admin/account/sessions` — every signed-in
  device with IP, OS · browser, created-at, last-seen.
- Correlation IDs on every request, surfaced in the
  `x-correlation-id` response header and threaded into audit rows.
- FK-hydrated list cells — `belongs_to` columns render as the target
  row's display field with click-throughs.
- Server-rendered templates baked into the binary; set
  `RUSTIO_TEMPLATE_DIR=templates` to override any page on disk.

## Adding a model

```sh
rustio startapp comment       # generates src/comment.rs +
                              # migrations/<NNNN>_create_comments.sql
```

The CLI prints the exact `mod` / `use` / `.model::<>()` lines to
paste into `src/main.rs`. After that, `rustio migrate apply` and
re-run; the `/admin/comments` pages light up.

> **Note:** `migrations/0001_create_posts.sql` already exists from
> the scaffold. Two valid paths from here: **(A)** keep the demo
> `Post` as a smoke test and add new migrations as `0002_*.sql`,
> `0003_*.sql`, … via `rustio startapp`; or **(B)** replace the demo
> with your real domain — delete `migrations/0001_create_posts.sql`
> and `src/post.rs`, remove the `Post` registration from
> `src/main.rs`, and write your real `0001_*.sql` instead. **Do this
> before the demo migration has been applied to a real database.**
> Once it has run in production, treat migrations as append-only and
> write a forward `0002_drop_posts.sql` instead.

## Project philosophy

- **Postgres-first.** Every query and every migration assumes
  Postgres semantics.
- **Operational clarity over magic.** No risk scoring, no AI
  heuristics, no automatic decisions the operator can't read.
- **Explicit model registration.** Models are listed by hand in
  `src/main.rs`; the admin sidebar matches the source code.
- **Server-rendered admin UI.** One Rust binary serves everything —
  no SPA, no separate frontend build step.
- **Security and auditability built into the lifecycle.** Authority
  guards, hashed-at-rest sessions, centralised invalidation, audit
  forensic chain — baked in.
- **No AI, no cloud lock-in, no frontend build step.** The same
  binary deploys to a $5 VPS, a kubernetes cluster, or an air-gapped
  factory floor.

For the full walkthrough see
[`docs/getting-started.md`](https://github.com/abdulwahed-sweden/rustio-admin/blob/main/docs/getting-started.md)
and the
[`ModelAdmin` reference](https://github.com/abdulwahed-sweden/rustio-admin/blob/main/docs/modeladmin.md).

## Project layout

| Path | Purpose |
|---|---|
| `src/main.rs`          | Boots the admin: connects to Postgres, applies migrations, registers models, mounts `/admin/*`. |
| `src/post.rs`          | Demo `Post` model with a populated `ModelAdmin` impl. Rename or delete when you stop needing the example. |
| `migrations/`          | Numerically prefixed `*.sql` files. `rustio migrate apply` runs every pending one transactionally. |
| `templates/admin/*.html` (optional) | Project-side overrides of any framework template. Set `RUSTIO_TEMPLATE_DIR=templates` to enable. |

## Operator commands

```sh
rustio doctor                              # health check
rustio migrate status                      # see what's applied / pending
rustio user list                           # show accounts
rustio user role --email <e> administrator # promote
rustio group create --name editors         # create a group
rustio perm grant-group --group editors --perm posts.add_post
```

## Multi-factor authentication (R3)

The framework ships TOTP-based MFA. Each user enrols at
`/admin/account/mfa/enroll` from their account menu:

1. The page renders an `otpauth://totp/...` QR code + a manual
   setup key (base32) — paste into any RFC 6238-compatible
   authenticator (Google Authenticator, 1Password, Bitwarden).
2. The user enters their first 6-digit code to confirm.
3. The framework AES-256-GCM-encrypts the secret with
   `RUSTIO_SECRET_KEY` (from `.env`) and stores it in
   `rustio_users.mfa_secret_ciphertext`. The plaintext never
   reaches stable storage.
4. 8 single-use backup codes print exactly once — record them
   out-of-band.

To make MFA mandatory for new users, add this line to the admin
chain in `src/main.rs`:

```rust
let admin = Admin::new()
    .require_mfa(rustio_admin::auth::MfaPolicy::Required)
    .model::<Post>();
```

Other policies: `Optional` (default), `RequiredForRoles(&[Role::Administrator])`,
`Disabled`.

`RUSTIO_SECRET_KEY` MUST be set whenever a user has MFA enabled.
See `.env.example` for the generation command.

## Emergency recovery (R4 CLI)

When every in-band recovery path is closed — the user forgot
their password AND lost their TOTP device AND there's no other
admin to drive an R2 reset — the operator with shell access to
the database runs:

```sh
# Set a new password + force rotation on next login. Revokes
# every active session.
rustio user reset-password \
  --email alice@{{name}}.local \
  --reason "Lost MFA device + locked out; no other admins available"

# Clear an auto-throttle lock.
rustio user unlock --email alice@{{name}}.local --reason "..."

# Drop MFA on a user (they re-enrol on next login if policy
# is Required).
rustio user disable-mfa --email alice@{{name}}.local --reason "..."

# Change a user's role. Refuses to demote the sole admin.
rustio user promote --email alice@{{name}}.local \
  --to-role administrator --reason "..."

# Issue a single-use password-reset URL (no email needed).
# Hand the URL to the target out-of-band.
rustio user emergency-access --email alice@{{name}}.local \
  --reason "..." --ttl-minutes 30
```

Every command renders a red ANSI confirmation banner before
mutating. Each writes one `rustio_admin_actions` row with
`action_type = "emergency_recovery"` so the audit trail shows
the operator went around every other tier. Pass `--yes` to skip
the prompt in scripts.

## Adding a custom route

`register_admin_routes` mounts the framework's admin surface.
Routes you want OUTSIDE that surface go directly on the
`Router` — mount them BEFORE `register_admin_routes` so the
framework's `/admin/*` wildcards never shadow them. Routes
share the same middleware chain (CSRF, correlation_id) as the
admin pages.

```rust
let db_for_inbox = db.clone();
let router = router.get("/inbox", move |req| {
    let db = db_for_inbox.clone();
    async move {
        // Your handler. Read DB via `db.pool()`. Read the CSRF
        // token off `req.ctx().get::<CsrfGuard>()`. Build a
        // response via `Response::html(...)` or `::text(...)`.
        Ok(rustio_admin::Response::text("project-specific page"))
    }
});

// Then mount the admin surface after your custom routes.
let router = register_admin_routes(router, admin, db, templates);
```

Want the custom route to require login? Either invoke the
framework's session lookup (`auth::session_token_from_cookie` +
`auth::identity_from_session`) at the top of your handler, or
write a small project-side helper like lursystem's
`auth_helper::require_role`. The framework's `login_guard` and
`role_guard` are currently `pub(crate)`; the public API will
expose them when the `pub guards` module lands.