Add set_password release command

608eff1 · gmorell · 2026-06-23 19:48

7 files +76 -0
Message
{commit_body(@commit)}

Files changed

modified lib/feed_pug/release.ex
+59 −0
@@ -73,6 +73,65 @@ defmodule FeedPug.Release do
73 73 end
74 74 end
75 75
76 + @doc """
77 + Sets (creates/resets) the password for an existing user, found by email. Useful when email
78 + delivery is down and the normal "forgot password" flow can't reach the user.
79 +
80 + Pass a password, or omit it to have a strong one generated and printed. All of the user's
81 + existing sessions/tokens are invalidated on change (standard phx.gen.auth behaviour).
82 +
83 + bin/feed_pug eval 'FeedPug.Release.set_password("user@example.com", "a-long-passphrase")'
84 + bin/feed_pug eval 'FeedPug.Release.set_password("user@example.com")' # generates one
85 + # or: bin/set-password user@example.com [password]
86 + """
87 + def set_password(email, password \\ nil) when is_binary(email) do
88 + load_app()
89 +
90 + {password, generated?} =
91 + case password do
92 + nil -> {random_password(), true}
93 + p when is_binary(p) -> {p, false}
94 + end
95 +
96 + {:ok, result, _} =
97 + Ecto.Migrator.with_repo(FeedPug.Repo, fn _repo ->
98 + case FeedPug.Accounts.get_user_by_email(email) do
99 + nil -> {:error, :not_found}
100 + user -> FeedPug.Accounts.update_user_password(user, %{password: password})
101 + end
102 + end)
103 +
104 + case result do
105 + {:ok, {_user, _expired_tokens}} ->
106 + IO.puts("✓ password set for #{email}")
107 + if generated?, do: IO.puts("\n generated password: #{password}\n")
108 + :ok
109 +
110 + {:error, :not_found} ->
111 + IO.puts("No user found with email #{inspect(email)}")
112 + System.halt(1)
113 +
114 + {:error, %Ecto.Changeset{} = changeset} ->
115 + IO.puts("Failed to set password:\n#{format_errors(changeset)}")
116 + System.halt(1)
117 + end
118 + end
119 +
120 + # A strong, URL-safe random password comfortably above the 12-char minimum.
121 + defp random_password do
122 + :crypto.strong_rand_bytes(18) |> Base.url_encode64(padding: false)
123 + end
124 +
125 + defp format_errors(changeset) do
126 + changeset
127 + |> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
128 + Enum.reduce(opts, msg, fn {key, value}, acc ->
129 + String.replace(acc, "%{#{key}}", to_string(value))
130 + end)
131 + end)
132 + |> Enum.map_join("\n", fn {field, msgs} -> " #{field}: #{Enum.join(msgs, ", ")}" end)
133 + end
134 +
76 135 defp repos do
77 136 Application.fetch_env!(@app, :ecto_repos)
78 137 end
added rel/overlays/bin/set-password
+17 −0
@@ -0,0 +1,17 @@
1 +#!/bin/sh
2 +set -eu
3 +
4 +cd -P -- "$(dirname -- "$0")"
5 +
6 +# Usage: bin/set-password <email> [password]
7 +# Generates a strong password when none is given (and prints it).
8 +if [ "$#" -lt 1 ]; then
9 + echo "usage: bin/set-password <email> [password]" >&2
10 + exit 2
11 +fi
12 +
13 +if [ "$#" -ge 2 ]; then
14 + exec ./feed_pug eval "FeedPug.Release.set_password(\"$1\", \"$2\")"
15 +else
16 + exec ./feed_pug eval "FeedPug.Release.set_password(\"$1\")"
17 +fi

Parents: f68263a