Init an admin user in prod closes #381 #409

Merged
moritz merged 14 commits from feature/381_init_admin into main 2026-02-04 20:53:02 +01:00
Showing only changes of commit 50c8a0dc9a - Show all commits

View file

@ -132,25 +132,15 @@ for attrs <- [
)
end
# Get admin email from environment variable or use default
# Admin email: default for dev/test so seed_admin has a target
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
System.put_env("ADMIN_EMAIL", admin_email)
# Admin password: use ADMIN_PASSWORD or ADMIN_PASSWORD_FILE if set; otherwise fallback
# only in dev/test (no fallback in production - prod uses Release.seed_admin in entrypoint)
get_admin_password = fn ->
from_file =
System.get_env("ADMIN_PASSWORD_FILE") |> then(fn path -> path && File.read(path) end)
from_env = System.get_env("ADMIN_PASSWORD")
case {from_file, from_env} do
{{:ok, content}, _} -> String.trim_trailing(content)
{_, p} when is_binary(p) and p != "" -> p
_ -> if Mix.env() in [:dev, :test], do: "testpassword", else: nil
# In dev/test, set fallback password so seed_admin creates the admin user when none is set
if Mix.env() in [:dev, :test] and is_nil(System.get_env("ADMIN_PASSWORD")) and
is_nil(System.get_env("ADMIN_PASSWORD_FILE")) do
System.put_env("ADMIN_PASSWORD", "testpassword")
end
end
admin_password = get_admin_password.()
# Create all authorization roles (idempotent - creates only if they don't exist)
# Roles are created using create_role_with_system_flag to allow setting is_system_role
@ -231,55 +221,9 @@ if is_nil(admin_role) do
raise "Failed to create or find admin role. Cannot proceed with member seeding."
end
# Assign admin role to user with ADMIN_EMAIL (if user exists)
# This handles both existing users (e.g., from OIDC) and newly created users.
# Password: use admin_password (from ENV or dev/test fallback); if nil, do not set password (prod-safe).
case Accounts.User
|> Ash.Query.filter(email == ^admin_email)
|> Ash.read_one(domain: Mv.Accounts, authorize?: false) do
{:ok, existing_admin_user} when not is_nil(existing_admin_user) ->
# User already exists (e.g., via OIDC) - set password if we have one, then assign admin role
user_after_password =
if is_binary(admin_password) and admin_password != "" do
existing_admin_user
|> Ash.Changeset.for_update(:admin_set_password, %{password: admin_password})
|> Ash.update!(authorize?: false)
else
existing_admin_user
end
user_after_password
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!(authorize?: false)
{:ok, nil} ->
# User doesn't exist - create admin user; set password only if we have one (no fallback in prod)
# Use authorize?: false for bootstrap - no admin user exists yet to use as actor
user =
Accounts.create_user!(%{email: admin_email},
upsert?: true,
upsert_identity: :unique_email,
authorize?: false
)
user =
if is_binary(admin_password) and admin_password != "" do
user
|> Ash.Changeset.for_update(:admin_set_password, %{password: admin_password})
|> Ash.update!(authorize?: false)
else
user
end
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|> Ash.update!(authorize?: false)
{:error, error} ->
raise "Failed to check for existing admin user: #{inspect(error)}"
end
# Create/update admin user via Release.seed_admin (uses ADMIN_EMAIL, ADMIN_PASSWORD / ADMIN_PASSWORD_FILE).
# Reduces duplication and exercises the same path as production entrypoint.
Mv.Release.seed_admin()
# Load admin user with role for use as actor in member operations
# This ensures all member operations have proper authorization
@ -781,8 +725,11 @@ IO.puts(" - Global settings: club_name = #{default_club_name}")
IO.puts(" - Membership fee types: 4 types (Yearly, Half-yearly, Quarterly, Monthly)")
IO.puts(" - Custom fields: 12 fields (String, Date, Boolean, Email, + 8 realistic fields)")
password_configured =
System.get_env("ADMIN_PASSWORD") != nil or System.get_env("ADMIN_PASSWORD_FILE") != nil
IO.puts(
" - Admin user: #{admin_email} (password: #{if admin_password, do: "set", else: "not set"})"
" - Admin user: #{admin_email} (password: #{if password_configured, do: "set", else: "not set"})"
)
IO.puts(" - Sample members: Hans, Greta, Friedrich")