auto-confirm admin during setup, skip email verification
Some checks failed
deploy / deploy (push) Has been cancelled
Some checks failed
deploy / deploy (push) Has been cancelled
Setup wizard no longer requires email delivery. Admin account is auto-confirmed and auto-logged-in via token redirect. Adds setup secret gate for prod (logged on boot), SMTP env var config in runtime.exs, email_configured? helper, and admin warning banner when email isn't set up. Includes plan files for this task and the follow-up email settings UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,36 +69,6 @@ defmodule Berrypod.Accounts do
|
||||
Repo.exists?(User)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the email of the admin user, or nil if no user exists.
|
||||
"""
|
||||
def admin_email do
|
||||
Repo.one(from u in User, select: u.email, limit: 1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the unconfirmed admin user, or nil.
|
||||
|
||||
Used by the setup wizard to allow "start over" before the magic link is clicked.
|
||||
"""
|
||||
def get_unconfirmed_admin do
|
||||
Repo.one(from u in User, where: is_nil(u.confirmed_at), limit: 1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes an unconfirmed user (confirmed_at is nil).
|
||||
|
||||
Returns `{:error, :already_confirmed}` if the user has already confirmed,
|
||||
preventing accidental deletion of an active admin account.
|
||||
"""
|
||||
def delete_unconfirmed_user(%User{confirmed_at: nil} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
def delete_unconfirmed_user(%User{}) do
|
||||
{:error, :already_confirmed}
|
||||
end
|
||||
|
||||
## User registration
|
||||
|
||||
@doc """
|
||||
@@ -119,6 +89,37 @@ defmodule Berrypod.Accounts do
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers and immediately confirms the admin user during setup.
|
||||
|
||||
Checks `has_admin?/0` inside a transaction to prevent race conditions.
|
||||
Returns `{:error, :admin_already_exists}` if an admin already exists.
|
||||
"""
|
||||
def register_and_confirm_admin(attrs) do
|
||||
Repo.transact(fn ->
|
||||
if has_admin?() do
|
||||
{:error, :admin_already_exists}
|
||||
else
|
||||
%User{}
|
||||
|> User.email_changeset(attrs)
|
||||
|> Ecto.Changeset.put_change(:confirmed_at, DateTime.utc_now(:second))
|
||||
|> Repo.insert()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a login token for a user without sending an email.
|
||||
|
||||
Used by the setup flow to create a token for the auto-login redirect.
|
||||
Returns the URL-safe encoded token.
|
||||
"""
|
||||
def generate_login_token(%User{} = user) do
|
||||
{encoded_token, user_token} = UserToken.build_email_token(user, "login")
|
||||
Repo.insert!(user_token)
|
||||
encoded_token
|
||||
end
|
||||
|
||||
## Settings
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -31,7 +31,23 @@ defmodule Berrypod.Application do
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Berrypod.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
result = Supervisor.start_link(children, opts)
|
||||
|
||||
log_setup_secret_if_needed()
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp log_setup_secret_if_needed do
|
||||
if Application.get_env(:berrypod, :env) != :test do
|
||||
unless Berrypod.Accounts.has_admin?() do
|
||||
secret = Berrypod.Setup.setup_secret()
|
||||
|
||||
require Logger
|
||||
|
||||
Logger.info("Setup secret: #{secret} — visit /setup to create your admin account")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Tell Phoenix to update the endpoint configuration
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
defmodule Berrypod.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :berrypod
|
||||
|
||||
@doc """
|
||||
Returns whether a real email adapter is configured.
|
||||
|
||||
True when the adapter is anything other than `Swoosh.Adapters.Local`
|
||||
(which just stores emails in memory for dev use).
|
||||
"""
|
||||
def email_configured? do
|
||||
adapter = Application.get_env(:berrypod, __MODULE__)[:adapter]
|
||||
adapter != nil and adapter != Swoosh.Adapters.Local
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,47 @@ defmodule Berrypod.Setup do
|
||||
|
||||
alias Berrypod.{Accounts, Orders, Products, Settings}
|
||||
|
||||
@setup_secret_key :berrypod_setup_secret
|
||||
|
||||
@doc """
|
||||
Returns the setup secret for first-run admin creation.
|
||||
|
||||
In prod, gates access to the setup form so only someone with server access
|
||||
(log output or SETUP_SECRET env var) can create the admin account.
|
||||
|
||||
Reads from SETUP_SECRET env var first, otherwise auto-generates a random
|
||||
token and stores it in :persistent_term for the lifetime of the node.
|
||||
"""
|
||||
def setup_secret do
|
||||
case System.get_env("SETUP_SECRET") do
|
||||
nil ->
|
||||
case :persistent_term.get(@setup_secret_key, nil) do
|
||||
nil ->
|
||||
secret =
|
||||
:crypto.strong_rand_bytes(16)
|
||||
|> Base.url_encode64(padding: false)
|
||||
|
||||
:persistent_term.put(@setup_secret_key, secret)
|
||||
secret
|
||||
|
||||
secret ->
|
||||
secret
|
||||
end
|
||||
|
||||
secret ->
|
||||
secret
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the setup secret gate should be shown.
|
||||
|
||||
True in prod when no admin exists. False in dev/test.
|
||||
"""
|
||||
def require_setup_secret? do
|
||||
Application.get_env(:berrypod, :env, :dev) == :prod and not Accounts.has_admin?()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a map describing the current setup status.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user