Best way to have app create a default user account when no user accounts are observed after launching?

Currently, during development, I am setting up a default user account using the seed file in my Phoenix application. When I eventually deploy this I would like the app to create the first default user account on its own when it observes no users in the database.

Where is the best place to do this?

I thought maybe the Application children list but that seems like a heavy solution for such a small one-off task.

I’m also considering just making a module called InitializeUsers and then having the expectation that after my first deploy I’ll remote iex into the app and just run this directly.

Thoughts?

1 Like

Yeah for this sort of thing I’d just remote in. If it isn’t complicated to create a user I don’t even know that you need a dedicated module for it.

You could do a check to see if there’s a db table and if not then run a production version for the seeds as a part of the deployment. But unless you do this all the time it will take more time to build than it would just consoling in and doing what you need by hand to ben’s point.

Good suggestions already, I’ll add an alternative: you can also integrate such code in your release script. So the user will always be there after a deployment.

I’m curious about this too as I’ve never launched anything and want to know how to create an admin/superuser when a site first gets launched. Currently I use the below just to avoid errors that arise if I ever drop the DB and just manually switch to admin if I need to.

@default_user %User{id: 0, username: "Anonymous"}

def mount(_params, session, socket) do
    socket = socket
    |> assign(:current_user, get_current_user(session))
    |> assign(:user, Map.get(socket, :user, @default_user))
    {:ok, socket}
  end

  def get_current_user(session) do
    case session do
      %{"user_token" => user_token} ->
        Accounts.get_user_by_session_token(user_token)
      %{} ->
        %User{id: 0, username: "Anonymous"}
    end
  end

Another alternative: I do this kind of initialization in a livebook. It’s a bit more work to connect to your instance with livebook than, say, starting a remote iex shell. But I find it to be a better experience, since you can have your little script sitting in a .livemd file, under source control, without poluting your production code. And the editing capabilities are better (IDE like) than an iex shell.

3 Likes

Another probably terrible idea is to just use an ecto migration. :troll:

Checkout how Plausible creates the super admin account!

The name, and password is provided via environment variable and the insertion happens by calling a bash script on the release.

Search the codebase with ADMIN_USER_ID

3 Likes

Hi @zorn !

I’ve seen this problem be handled a few different ways:

    1. add a migration for it
    1. create a scripts folder and add name things as needed: one_time_create_user.ex, remove_some_resource.ex and call them from an IEx session
      in prod.
    1. create script files that would be shared with different departments so they would run it instead of you due to compliance reasons, here is
      the flow → create a ticket for a department (devops) to run it for you, add the script to the a ticket so work is tracked, they run it for yo
      u, sometimes give you an output if any (pipe it to a file). This adds an audit trail and is related to compliance and auditing.
    1. create a repository for these scripts → inside this repo, create folders for the different apps/project names and inside those, add scrip
      ts that you want run. A script to be run becomes a PR against this repo, where CI would spin up the app (knows which one due to folder) and pump the script (only diff between your branch and main) into an IEx session. This is the most audit compliant way I’ve seen of handling these requests. PRs get reviewed, tickets get linked to it, etc. This scales well and all parties are happy (devs, devops, compliance, etc)

Each has tradeoffs. I know your example is a small one, but I figured I’d share and maybe create a discussion. On each of them:

First, a script would be a module that would have some docs on why it would be called/frequency, etc. CreateUserAtBootIfNotFound or something, exposing a function (call/1 or something) and a then a function call that executes it:

# scripts/one_time_create_user_at_boot_if_not_found.ex
defmodule CreateUserAtBootIfNotFound do
  def call(args) do
    ...
  end
end

CreateUserAtBootIfNotFound.call(%{})

On each of the above:

  1. don’t use migrations. When I saw this, it was a long time ago and also not even in the Phoenix ecossystem. A few issues with these, mostly because modules and implementations can change and you can be in a spot where migrations start to cry. I felt like I should mention it, mostly to say that there were other solutions.

  2. if it is a recurring task and you don’t know the problem that well, you may want this one while you research how often (if needed at all) this would be run.

  3. this was v0 of trying to get compliance and dealing with scale. Not the best, not the worst. Annoying over time.

  4. this was the best one, but you need some structure and things in place to get it going. My favorite and best if you need to run at scale. You can add linter checks if you’d like, a lot of control as well as relying on PR reviews (adds ownership).

For your situation, I think creating a scripts/your_script.ex could work. Mostly because you may want that script handy for testing and dev.

Am I missing one? What are your thoughts? Hope this helps.

Edit (I’ll keep edits here):

  • Livebook! Nice! That’s an interesting solution.
2 Likes