All checks were successful
deploy / deploy (push) Successful in 42s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
201 lines
8.8 KiB
Markdown
201 lines
8.8 KiB
Markdown
# 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
|