# Setup wizard: auto-confirm admin, setup secret gate Status: Planned 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: — 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