# Admin accounts, password reset, and Steam

## First-admin bootstrap

On every start the controller checks the `users` table
(`controller/cmd/controller/auth.go`, `bootstrapAdmin`). If it's
**empty**, it creates:

- email: `admin@panel.local`
- role: `admin`
- password: **randomly generated** and printed to the log inside a loud
  `ADMIN BOOTSTRAP` banner (systemd: `journalctl -u panel-controller`;
  compose: `docker compose logs controller`). Log in once and change it
  via the UI.

To pre-seed a stable password instead (dev, CI, unattended installs),
set `PANEL_ADMIN_PASSWORD` in the controller's environment before first
boot. The banner then says the password came from the env var and never
prints it.

Sessions are cookie-based (`panel_session`); passwords are stored as
argon2id hashes.

## Resetting a lost admin password

Use the `reset-admin` tool (`controller/cmd/reset-admin`). It writes the
DB directly, so it works even when you can't log in:

```bash
go build -o bin/reset-admin ./controller/cmd/reset-admin   # or use a prebuilt copy

./bin/reset-admin --email admin@panel.local --password 'NewS3cret' \
  --db-url 'postgres://panel:...@127.0.0.1:5432/panel?sslmode=disable'
# → reset-admin: password updated for admin@panel.local
```

`--db-url` falls back to `$PANEL_DB_URL`, then to the controller's dev
default DSN. The controller does **not** need a restart — the next login
verifies against the new hash. Existing sessions stay valid; log out /
delete rows from `sessions` if you want them revoked.

Hash-only mode (no DB access from the tool — e.g. you only have `psql`):

```bash
./bin/reset-admin 'NewS3cret'
# → $argon2id$v=19$m=65536,t=3,p=2$...$...
```

then apply it manually:

```sql
UPDATE users
   SET password_hash = '$argon2id$v=19$m=65536,t=3,p=2$...$...',
       updated_at    = NOW()
 WHERE email = 'admin@panel.local';
```

(Quote the hash in single quotes — it contains `$`.)

With the managed `panel-postgres` container:

```bash
docker exec -it panel-postgres psql -U panel -d panel
```

## Adding more users

The account you set at install is the **first admin**. To add teammates,
open the **🎮 Steam Accounts** panel (sidebar) — this is the user-management
screen. As an admin you can:

- **List** every panel user and their linked Steam ID.
- **Add a user** by their 17-digit **SteamID64**, as role `user` or `admin`
  (`POST /api/users`). They then log in with **Sign in with Steam** — no
  password for you to manage or hand out.
- **Link / unlink** a Steam ID on any account.

The intended model: you (the install admin) sign in with your email +
password; everyone else you add is **Steam-based**. That's ideal for a
gaming community — players already have Steam.

**Limitation, stated plainly:** there is currently **no UI/API to create
additional *email + password* users** — `POST /api/users` is Steam-only.
If you need a second non-Steam login, add it at the database level or with
`reset-admin --email <addr> --password <pw>` (see
[Resetting a lost admin password](#resetting-a-lost-admin-password)). A
proper "invite a password user" flow is a known follow-up.

## Sign in with Steam (operator login)

Optional OpenID login for the dashboard. Two controller flags:

- `-public-url https://panel.example.com` — the public base URL of the
  dashboard. Steam's OpenID flow redirects back to
  `<public-url>/api/auth/steam/callback`, so this must be the URL your
  browser actually uses (reverse-proxied name included).
- `-initial-admin-steam-id 7656119…` — your 17-digit SteamID64.
  - Empty DB → the bootstrap admin is created with this Steam ID linked;
    "Sign in with Steam" works from the first boot.
  - Existing DB → the first admin gets retro-linked, **if** it has no
    Steam ID yet (an existing different link is never clobbered).

Users can also link/unlink Steam on their own account from the UI
(`/api/auth/steam/link-start`). Steam-linked users may sign in with
either method; Steam-only users (created by an admin via
`POST /api/users`) have no password at all.

Find your SteamID64 at steamid.io or via your profile URL.

## Steam credentials for paid games (`requires_steam_login`)

Some dedicated servers refuse SteamCMD's `+login anonymous` — DayZ
(app 223350) is the shipped example; its provider declares
`requires_steam_login: true` in `modules/dayz/module.yaml`. Those
downloads need a real Steam account **that owns the game**.

How you supply them — you don't pre-configure anything:

1. Click **Update** (or create) on such an instance.
2. No cached credential → the API answers
   `{"steam_login_required": true, ...}` and the dashboard pops the
   **Steam login modal**.
3. Enter Steam username + password. If the account has Steam Guard,
   the first attempt returns `{"need_guard": true}` and the modal
   re-prompts for the emailed / mobile-authenticator code.
4. The controller runs a one-shot SteamCMD sidecar to validate the
   login, then re-runs your original update with the now-cached
   credential.

Storage (see the header of `controller/cmd/controller/steamauth.go`):

- The password is encrypted at rest with AES-GCM; the key is
  HKDF-derived from the internal CA private key (so restoring
  `data/ca` + the Postgres dump restores decryptability — no separate
  key file).
- The Steam Guard **sentry** blob is stored and mounted into every later
  SteamCMD run, so 2FA is a one-time ceremony per account.
- The password is never logged and never sent back to the UI.

Managing stored accounts: `GET /api/steam/accounts` lists them (no
secrets), `DELETE /api/steam/accounts` removes one. Use a throwaway
account that owns only the games you host if you're uncomfortable
storing your main account's credential.

## Out-of-band admin (dashboard is broken)

A separate token-authed HTTP surface (`/admin/v1/*`) plus
`panelctl admin` exist for repair when the UI is down — see the
"Admin HTTP" section of the [README](../README.md). Auth is loopback
bypass on the controller host, or the `X-Panel-Admin-Token` header with
the token from `<data-dir>/admin-token` (auto-generated, mode 0600;
`panelctl admin token-show` prints it).
