From acb33b9f3b71b45f6a99446301dba6069baea2f3 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 27 Jan 2026 14:29:01 +0100 Subject: [PATCH] Ensure system actor user exists via migration Creates user system@mila.local with Admin role if missing. Idempotent; guarantees system actor in production without relying on seeds. --- ...124937_ensure_system_actor_user_exists.exs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 priv/repo/migrations/20260127124937_ensure_system_actor_user_exists.exs diff --git a/priv/repo/migrations/20260127124937_ensure_system_actor_user_exists.exs b/priv/repo/migrations/20260127124937_ensure_system_actor_user_exists.exs new file mode 100644 index 0000000..b0ee775 --- /dev/null +++ b/priv/repo/migrations/20260127124937_ensure_system_actor_user_exists.exs @@ -0,0 +1,59 @@ +defmodule Mv.Repo.Migrations.EnsureSystemActorUserExists do + @moduledoc """ + Ensures the system actor user always exists. + + The system actor is used for systemic operations (email sync, cycle generation, + background jobs). It is created by seeds in development; in production seeds + may not run, so this migration guarantees the user exists. + + Creates a user with email "system@mila.local" (default from Mv.Helpers.SystemActor) + and the Admin role. The user has no password and no OIDC ID, so it cannot log in. + """ + use Ecto.Migration + import Ecto.Query + + @system_user_email "system@mila.local" + + def up do + admin_role_id = ensure_admin_role_exists() + ensure_system_actor_user_exists(admin_role_id) + end + + def down do + # Not reversible - do not delete system user on rollback + :ok + end + + defp ensure_admin_role_exists do + case repo().one(from(r in "roles", where: r.name == "Admin", select: r.id)) do + nil -> + execute(""" + INSERT INTO roles (id, name, description, permission_set_name, is_system_role, inserted_at, updated_at) + VALUES (uuid_generate_v7(), 'Admin', 'Administrator with full access', 'admin', false, (now() AT TIME ZONE 'utc'), (now() AT TIME ZONE 'utc')) + """) + + role_id = repo().one(from(r in "roles", where: r.name == "Admin", select: r.id)) + IO.puts("✅ Created 'Admin' role (was missing)") + role_id + + role_id -> + role_id + end + end + + defp ensure_system_actor_user_exists(_admin_role_id) do + case repo().one(from(u in "users", where: u.email == ^@system_user_email, select: u.id)) do + nil -> + execute(""" + INSERT INTO users (id, email, hashed_password, oidc_id, member_id, role_id) + SELECT gen_random_uuid(), '#{@system_user_email}', NULL, NULL, NULL, r.id + FROM roles r WHERE r.name = 'Admin' LIMIT 1 + """) + + IO.puts("✅ Created system actor user (#{@system_user_email})") + + _ -> + :ok + end + end +end