berrypod/docs/plans/setup-auto-confirm.md

201 lines
8.8 KiB
Markdown
Raw Normal View History

# Setup wizard: auto-confirm admin, setup secret gate
Status: Complete
Companion plan: [email-settings.md](email-settings.md) (admin UI for email config)
## Context
The setup wizard requires email verification (magic link) to create the admin account. This doesn't work for self-hosted users without email configured. The fix: auto-confirm and auto-login the first admin during setup, gated by a setup secret in prod. Also adds env var SMTP support and a warning banner when email isn't configured.
## UX flow
### Screen states
| Context | What they see |
|---------|---------------|
| Prod, no admin, no secret entered | Secret gate only (single input + submit) |
| Prod, no admin, secret verified | All 3 cards (card 1 active with email field) |
| Dev, no admin | All 3 cards (card 1 active, no secret gate) |
| Admin exists, not logged in | Redirect to `/users/log-in` |
| Logged in, setup incomplete | All 3 cards (card 1 done with checkmark, cards 2+3 active) |
| Setup complete | Redirect to `/admin` |
| Site live | Redirect to `/` |
### Prod first-run flow
1. Server logs: `Setup secret: abc123-def456 — visit /setup to create your admin account`
2. Visit `/setup` -> see only a setup secret input
3. Enter correct secret -> LiveView assigns `secret_verified: true` -> template reveals all 3 cards
4. Card 1 "Create admin account" has email field -> submit -> account created, auto-confirmed, token generated, redirect to `/setup/login/:token` -> controller sets session cookie -> redirect back to `/setup` -> card 1 shows done, cards 2+3 active
### Dev first-run flow
1. Visit `/setup` -> all 3 cards visible immediately (no secret gate)
2. Card 1 has email field -> submit -> same auto-confirm + redirect flow
## Changes
### 1. Setup secret (`lib/berrypod/setup.ex`)
Add to existing file:
- `setup_secret/0` — returns `SETUP_SECRET` env var, or auto-generates via `:crypto.strong_rand_bytes/1` on first call and stores in `:persistent_term`. Not stored in DB (bootstrap credential needed before admin exists).
- `require_setup_secret?/0``true` in prod when no admin exists. `false` in dev.
### 2. Log setup secret on boot (`lib/berrypod/application.ex`)
In `start/2`, after children are started: if no admin exists, call `Setup.setup_secret/0` and log it:
```
[info] Setup secret: <token> — visit /setup to create your admin account
```
### 3. Auto-confirm admin registration (`lib/berrypod/accounts.ex`)
Add `register_and_confirm_admin/1`:
- Takes `%{email: email}` attrs
- Inside `Repo.transaction`:
1. Check `Setup.has_admin?()` — if true, rollback with `:admin_already_exists`
2. Build user changeset with `confirmed_at: DateTime.utc_now()`
3. Insert user
4. Return `{:ok, user}`
Remove dead functions (only used by old check_inbox flow): `admin_email/0`, `get_unconfirmed_admin/0`, `delete_unconfirmed_user/1`
### 4. Setup login controller (`lib/berrypod_web/controllers/setup_controller.ex` — new)
Needed because LiveViews can't set session cookies.
- `login/2` action:
1. Decode token, call `Accounts.login_user_by_magic_link/1` to consume it
2. On success: `UserAuth.log_in_user(conn, user)` (creates session, redirects to `/setup`)
3. On failure: redirect to `/setup` with error flash
### 5. Route (`lib/berrypod_web/router.ex`)
Add in a `:browser` scope (outside any live_session):
```elixir
get "/setup/login/:token", SetupController, :login
```
### 6. Rewrite onboarding LiveView (`lib/berrypod_web/live/setup/onboarding.ex`)
**Mount logic:**
```elixir
cond do
setup.site_live -> redirect /
setup.setup_complete -> redirect /admin
setup.admin_created and not logged_in -> redirect /users/log-in
true -> mount setup page
end
```
**Assigns on mount:**
- `require_secret?` from `Setup.require_setup_secret?/0`
- `secret_verified: false`
- `secret_form` (simple form for the gate)
- `email_form` (for card 1)
- All existing configure-phase assigns
**Events:**
- `verify_secret` — validates against `Setup.setup_secret/0`, sets `secret_verified: true` or flashes error
- `create_account` — validates email, calls `register_and_confirm_admin/1`, generates login token (reuses `UserToken.build_email_token/2` pattern but without sending email), redirects to `/setup/login/:token`
**Template structure:**
- If `require_secret?` and not `secret_verified`: show secret gate only
- Otherwise: show all 3 cards. Card 1 state depends on `logged_in?`
**Remove:** `:email_form` and `:check_inbox` phases, `mount_email_form/1`, `mount_check_inbox/1`, `start_over` event, `local_mail_adapter?/0`
### 7. SMTP env var support (`config/runtime.exs`)
Conditional SMTP adapter config (consumed at boot):
```elixir
if smtp_host = System.get_env("SMTP_HOST") do
config :berrypod, Berrypod.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: smtp_host,
port: String.to_integer(System.get_env("SMTP_PORT") || "587"),
username: System.get_env("SMTP_USERNAME"),
password: System.get_env("SMTP_PASSWORD"),
tls: :if_available
end
```
This is a simple fallback for users who want env-based config. The companion plan (email-settings.md) adds a full admin UI with multi-adapter support.
### 8. Email configured helper (`lib/berrypod/mailer.ex`)
Add `email_configured?/0` — returns `true` if the adapter is anything other than `Swoosh.Adapters.Local`.
### 9. Admin warning banner
**`lib/berrypod_web/admin_layout_hook.ex`:**
- Add `email_configured` assign via `Berrypod.Mailer.email_configured?/0`
**`lib/berrypod_web/components/layouts/admin.html.heex`:**
- Warning banner above `{@inner_content}` when `!@email_configured`:
"Email delivery not configured. Customers won't receive order confirmations. Set SMTP_HOST to enable email."
- Later (after email-settings plan): this links to the email settings page.
**`assets/css/admin/components.css`:**
- `.admin-banner-warning` style (amber background, icon)
### 10. Verify `/dev/mailbox` link (`lib/berrypod_web/live/auth/login.ex`)
Check that the `/dev/mailbox` link is already conditional on `Swoosh.Adapters.Local`. No changes expected — just verify.
### 11. Tests
**`test/berrypod/accounts_test.exs`:**
- Add tests for `register_and_confirm_admin/1` (success, admin already exists)
- Remove tests for deleted functions
**`test/berrypod_web/live/setup/onboarding_test.exs`:**
- Remove: check_inbox tests, start_over test
- Add: secret gate rejects wrong secret
- Add: secret gate accepts correct secret
- Add: creating account auto-confirms and redirects
- Add: admin exists but not logged in redirects to login page
**`test/berrypod_web/controllers/setup_controller_test.exs` (new):**
- Valid token logs in and redirects to `/setup`
- Invalid/expired token redirects with error flash
## Files to modify
| File | Change |
|------|--------|
| `lib/berrypod/setup.ex` | Add `setup_secret/0`, `require_setup_secret?/0` |
| `lib/berrypod/application.ex` | Log setup secret on boot |
| `lib/berrypod/accounts.ex` | Add `register_and_confirm_admin/1`, remove 3 dead fns |
| `lib/berrypod/mailer.ex` | Add `email_configured?/0` |
| `lib/berrypod_web/live/setup/onboarding.ex` | Remove check_inbox, add secret gate + auto-confirm |
| `lib/berrypod_web/controllers/setup_controller.ex` | New — session creation endpoint |
| `lib/berrypod_web/router.ex` | Add setup login route |
| `lib/berrypod_web/admin_layout_hook.ex` | Add `email_configured` assign |
| `lib/berrypod_web/components/layouts/admin.html.heex` | Warning banner |
| `assets/css/admin/components.css` | `.admin-banner-warning` style |
| `config/runtime.exs` | Conditional SMTP config |
| Tests (3 files) | Update for new flow |
## Security notes
- **Setup secret** prevents unauthorized admin creation on fresh instances. Only someone with server log access or the `SETUP_SECRET` env var can proceed.
- **Not stored in DB** — it's a bootstrap credential needed before the admin exists. `:persistent_term` for auto-generated, env var for explicit.
- **Skipped in dev** — no friction during local development.
- **Skipped when logged in** — hosted platform users are already authenticated.
- **Auto-login token** is same-origin (never emailed), single-use (consumed immediately by controller), with 15-minute expiry as safety net.
- **Race-safe** — `register_and_confirm_admin/1` checks `has_admin?()` inside a DB transaction.
- **Removes info leak** — old check_inbox phase exposed admin email to unauthenticated visitors.
## Verification
1. `mix precommit` passes
2. Dev fresh install: visit `/setup`, enter email only, immediately on configure phase
3. Prod fresh install: secret gate shown first, wrong secret rejected, correct secret reveals email form, submit -> auto-logged in -> configure phase
4. Admin pages: warning banner shows when SMTP not configured
5. With `SMTP_HOST` set: banner hidden, `Mailer.email_configured?()` returns true
6. Login page: `/dev/mailbox` link shows only with Local adapter
7. All setup tests pass