From 77012f10caf185f8aeed5dc0b5028f100842c1a7 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 16 Mar 2026 19:20:30 +0100 Subject: [PATCH] refactor: harden seed only once implementation --- .env.example | 1 + CHANGELOG.md | 3 ++- CODE_GUIDELINES.md | 4 ++-- docs/admin-bootstrap-and-oidc-role-sync.md | 5 +++-- lib/mv/release.ex | 13 ++++++++----- priv/repo/seeds.exs | 7 ++++--- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 661593b..d63e019 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,7 @@ ASSOCIATION_NAME="Sportsclub XYZ" # Optional: Admin user (created/updated on container start via Release.seed_admin) # In production, set these so the first admin can log in. Change password without redeploy: # bin/mv eval "Mv.Release.seed_admin()" (with new ADMIN_PASSWORD or ADMIN_PASSWORD_FILE) +# FORCE_SEEDS=true re-runs bootstrap seeds even when admin user exists (e.g. after changing roles/custom fields). # ADMIN_EMAIL=admin@example.com # ADMIN_PASSWORD=secure-password # ADMIN_PASSWORD_FILE=/run/secrets/admin_password diff --git a/CHANGELOG.md b/CHANGELOG.md index e54d132..92319c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **FORCE_SEEDS** – Environment variable. When set to `"true"`, bootstrap (and optionally dev) seeds are run even when the admin user already exists, so you can re-apply changed seed data (e.g. new roles or custom fields) without deleting the admin user. - **Improved OIDC-only mode** – Admin can enable “Only OIDC sign-in” in settings; when enabled, direct registration is disabled and sign-in page redirects to OIDC when configured. - **Success toast auto-dismiss** – Success flash messages (e.g. “Settings saved”) hide automatically after 5 seconds instead of requiring the user to close them. ### Changed -- **Seeds run only when needed** – Bootstrap and dev seeds are skipped on application start when the admin user already exists (`Mv.Release.bootstrap_seeds_applied?/0`). This avoids duplicate data and speeds up startup in dev and production after the first run. +- **Seeds run only when needed** – Bootstrap and dev seeds are skipped on application start when the admin user already exists (`Mv.Release.bootstrap_seeds_applied?/0`). This avoids duplicate data and speeds up startup in dev and production after the first run. Set `FORCE_SEEDS=true` to override and re-run. - **Unauthenticated access** – Users who are not logged in are redirected to sign-in without showing a “no permission” message; the message is only shown to logged-in users who lack access. ### Fixed diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index 911c8d9..f84c5ad 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -286,11 +286,11 @@ end Seeds are split into **bootstrap** and **dev**. They run on every start (e.g. `just run`, Docker entrypoint) but **exit early** if already applied so startup stays fast and no duplicate data is created. -- **`priv/repo/seeds.exs`** – Entrypoint. If the admin user (ADMIN_EMAIL or default) already exists, skips entirely; otherwise runs `seeds_bootstrap.exs` and, in dev/test, `seeds_dev.exs`. +- **`priv/repo/seeds.exs`** – Entrypoint. If the admin user (ADMIN_EMAIL or default) already exists, skips entirely (unless `FORCE_SEEDS=true`); otherwise runs `seeds_bootstrap.exs` and, in dev/test, `seeds_dev.exs`. - **`priv/repo/seeds_bootstrap.exs`** – Creates only data required for system startup: membership fee types, custom fields, roles, admin user, system user, global settings (including default membership fee type). No members, no groups. Used in all environments (dev, test, prod). - **`priv/repo/seeds_dev.exs`** – Creates 20 sample members, groups, and optional custom field values. Run only in dev and test. -In production, running `mix run priv/repo/seeds.exs` (or `Mv.Release.run_seeds/0`) executes only the bootstrap part when not yet applied (no dev seeds unless `RUN_DEV_SEEDS=true`). The “already applied” check uses `Mv.Release.bootstrap_seeds_applied?/0` (admin user exists). +In production, running `mix run priv/repo/seeds.exs` (or `Mv.Release.run_seeds/0`) executes only the bootstrap part when not yet applied (no dev seeds unless `RUN_DEV_SEEDS=true`). The “already applied” check uses `Mv.Release.bootstrap_seeds_applied?/0` (admin user exists). Set `FORCE_SEEDS=true` to re-run seeds even when already applied. ### 1.3 Domain-Driven Design diff --git a/docs/admin-bootstrap-and-oidc-role-sync.md b/docs/admin-bootstrap-and-oidc-role-sync.md index 0998d67..21bf947 100644 --- a/docs/admin-bootstrap-and-oidc-role-sync.md +++ b/docs/admin-bootstrap-and-oidc-role-sync.md @@ -2,7 +2,7 @@ ## Overview -- **Admin bootstrap:** In production, the Docker entrypoint runs migrate, then `Mv.Release.run_seeds/0` (skips if admin user already exists; otherwise runs bootstrap seeds; set `RUN_DEV_SEEDS=true` to also run dev seeds), then `seed_admin/0` from ENV, then the server. Password can be changed without redeploy via `bin/mv eval "Mv.Release.seed_admin()"`. +- **Admin bootstrap:** In production, the Docker entrypoint runs migrate, then `Mv.Release.run_seeds/0` (skips if admin user already exists unless `FORCE_SEEDS=true`; set `RUN_DEV_SEEDS=true` to also run dev seeds), then `seed_admin/0` from ENV, then the server. Password can be changed without redeploy via `bin/mv eval "Mv.Release.seed_admin()"`. - **OIDC role sync:** Optional mapping from OIDC groups (e.g. from Authentik profile scope) to the Admin role. Users in the configured admin group get the Admin role on registration and on each sign-in. ## Admin Bootstrap (Part A) @@ -10,13 +10,14 @@ ### Environment Variables - `RUN_DEV_SEEDS` – If set to `"true"`, `run_seeds/0` also runs dev seeds (members, groups, sample data). Otherwise only bootstrap seeds run. +- `FORCE_SEEDS` – If set to `"true"`, seeds are run even when the admin user already exists (e.g. after changing bootstrap data such as roles or custom fields). Otherwise seeds are skipped when bootstrap was already applied. - `ADMIN_EMAIL` – Email of the admin user to create/update. If unset, seed_admin/0 does nothing. - `ADMIN_PASSWORD` – Password for the admin user. If unset (and no file), no new user is created; if a user with ADMIN_EMAIL already exists (e.g. OIDC-only), their role is set to Admin (no password change). - `ADMIN_PASSWORD_FILE` – Path to a file containing the password (e.g. Docker secret). ### Release Tasks -- `Mv.Release.run_seeds/0` – If the admin user already exists (bootstrap already applied), skips; otherwise runs bootstrap seeds (fee types, custom fields, roles, settings). If `RUN_DEV_SEEDS` env is `"true"`, also runs dev seeds (members, groups, sample data). Safe to call on every start. +- `Mv.Release.run_seeds/0` – If the admin user already exists (bootstrap already applied), skips unless `FORCE_SEEDS=true`; otherwise runs bootstrap seeds (fee types, custom fields, roles, settings). If `RUN_DEV_SEEDS` env is `"true"`, also runs dev seeds (members, groups, sample data). Safe to call on every start. - `Mv.Release.seed_admin/0` – Reads ADMIN_EMAIL and password from ADMIN_PASSWORD or ADMIN_PASSWORD_FILE. If both email and password are set: creates or updates the user with the Admin role. If only ADMIN_EMAIL is set: sets the Admin role on an existing user with that email (for OIDC-only admins); does not create a user. Idempotent. ### Entrypoint diff --git a/lib/mv/release.ex b/lib/mv/release.ex index 7b30c83..116b276 100644 --- a/lib/mv/release.ex +++ b/lib/mv/release.ex @@ -7,7 +7,7 @@ defmodule Mv.Release do - `migrate/0` - Runs all pending Ecto migrations. - `bootstrap_seeds_applied?/0` - Returns whether bootstrap was already applied (admin user exists). Used to skip re-running seeds. - - `run_seeds/0` - If bootstrap already applied, skips; otherwise runs bootstrap seeds (fee types, custom fields, roles, settings). In production, set `RUN_DEV_SEEDS=true` to also run dev seeds (members, groups, sample data). + - `run_seeds/0` - If bootstrap already applied, skips; otherwise runs bootstrap seeds (fee types, custom fields, roles, settings). Set `FORCE_SEEDS=true` to re-run seeds even when already applied. In production, set `RUN_DEV_SEEDS=true` to also run dev seeds (members, groups, sample data). - `seed_admin/0` - Ensures an admin user exists from ENV (ADMIN_EMAIL, ADMIN_PASSWORD or ADMIN_PASSWORD_FILE). Idempotent; can be run on every deployment or via shell to update the admin password without redeploying. @@ -19,6 +19,7 @@ defmodule Mv.Release do alias Mv.Authorization.Role require Ash.Query + require Logger def migrate do load_app() @@ -46,13 +47,15 @@ defmodule Mv.Release do _ -> false end rescue - _ -> false + e -> + Logger.warning("Could not check seed status (#{inspect(e)}), assuming not applied.") + false end @doc """ Runs seed scripts so the database has required bootstrap data (and optionally dev data). - - Skips if bootstrap was already applied (Admin role exists); otherwise runs bootstrap seeds. + - Skips if bootstrap was already applied (admin user exists); set `FORCE_SEEDS=true` to override and re-run. - If `RUN_DEV_SEEDS` env is set to `"true"`, also runs dev seeds (members, groups, sample data) when bootstrap is run. @@ -64,8 +67,8 @@ defmodule Mv.Release do {:error, {app, reason}} -> raise "Failed to start #{inspect(app)}: #{inspect(reason)}" end - if bootstrap_seeds_applied?() do - IO.puts("Seeds already applied (admin user exists). Skipping.") + if bootstrap_seeds_applied?() and System.get_env("FORCE_SEEDS") != "true" do + IO.puts("Seeds already applied. Skipping. (Set FORCE_SEEDS=true to override)") else priv = :code.priv_dir(@app) bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs") diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index fad2982..c562a43 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -4,7 +4,8 @@ # # Bootstrap runs in all environments. Dev seeds (members, groups, sample data) # run only in dev and test. Skips entirely if bootstrap was already applied -# (Admin role exists), so safe to run on every start. +# (admin user exists), so safe to run on every start. Set FORCE_SEEDS=true to +# re-run seeds even when already applied. # # In production (release): seeds are run via Mv.Release.run_seeds/0 from the # container entrypoint. Set RUN_DEV_SEEDS=true to also run dev seeds there. @@ -15,8 +16,8 @@ _ = Application.ensure_all_started(:mv) -if Mv.Release.bootstrap_seeds_applied?() do - IO.puts("Seeds already applied (admin user exists). Skipping.") +if Mv.Release.bootstrap_seeds_applied?() and System.get_env("FORCE_SEEDS") != "true" do + IO.puts("Seeds already applied. Skipping. (Set FORCE_SEEDS=true to override)") else prev = Code.compiler_options() Code.compiler_options(ignore_module_conflict: true)