feat: set password for new and for existing user

This commit is contained in:
Moritz 2025-07-22 22:12:43 +02:00 committed by moritz
parent 2e256a0206
commit 662e80cc74
4 changed files with 218 additions and 21 deletions

View file

@ -71,6 +71,19 @@ defmodule Mv.Accounts.User do
accept [:email] accept [:email]
end end
# Admin action for direct password changes in admin panel
# Uses the official Ash Authentication HashPasswordChange with correct context
update :admin_set_password do
accept [:email]
argument :password, :string, allow_nil?: false, sensitive?: true
# Set the strategy context that HashPasswordChange expects
change set_context(%{strategy_name: :password})
# Use the official Ash Authentication password change
change AshAuthentication.Strategy.Password.HashPasswordChange
end
read :get_by_subject do read :get_by_subject do
description "Get a user by the subject claim in a JWT" description "Get a user by the subject claim in a JWT"
argument :subject, :string, allow_nil?: false argument :subject, :string, allow_nil?: false
@ -121,6 +134,14 @@ defmodule Mv.Accounts.User do
identity :unique_oidc_id, [:oidc_id] identity :unique_oidc_id, [:oidc_id]
end end
# Global validations - applied to all relevant actions
validations do
# Password strength policy: minimum 8 characters for all password-related actions
validate string_length(:password, min: 8) do
where action_is([:register_with_password, :admin_set_password])
end
end
# You can customize this if you wish, but this is a safe default that # You can customize this if you wish, but this is a safe default that
# only allows user data to be interacted with via AshAuthentication. # only allows user data to be interacted with via AshAuthentication.
# policies do # policies do

View file

@ -13,23 +13,81 @@ defmodule MvWeb.UserLive.Form do
<.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.form for={@form} id="user-form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label={gettext("Email")} required type="email" /> <.input field={@form[:email]} label={gettext("Email")} required type="email" />
<%= if @user do %> <!-- Password Section -->
<div class="mt-4 p-4 bg-blue-50 rounded-lg"> <div class="mt-6">
<p class="text-sm text-blue-800"> <label class="flex items-center space-x-2">
<strong>{gettext("Note")}:</strong> {gettext( <input
"Password can only be changed through authentication functions." type="checkbox"
)} name="set_password"
</p> phx-click="toggle_password_section"
</div> checked={@show_password_fields}
<% else %> class="checkbox checkbox-sm"
<div class="mt-4 p-4 bg-yellow-50 rounded-lg"> />
<p class="text-sm text-yellow-800"> <span class="text-sm font-medium">
<strong>{gettext("Note")}:</strong> {gettext( {if @user, do: gettext("Change Password"), else: gettext("Set Password")}
"Users created here will need to set their password through the authentication system." </span>
)} </label>
</p>
</div> <%= if @show_password_fields do %>
<% end %> <div class="mt-4 space-y-4 p-4 bg-gray-50 rounded-lg">
<.input
field={@form[:password]}
label={gettext("Password")}
type="password"
required
autocomplete="new-password"
/>
<!-- Only show password confirmation for new users (register_with_password) -->
<%= if !@user do %>
<.input
field={@form[:password_confirmation]}
label={gettext("Confirm Password")}
type="password"
required
autocomplete="new-password"
/>
<% end %>
<div class="text-sm text-gray-600">
<p><strong>{gettext("Password requirements")}:</strong></p>
<ul class="list-disc list-inside text-xs mt-1 space-y-1">
<li>{gettext("At least 8 characters")}</li>
<li>{gettext("Include both letters and numbers")}</li>
<li>{gettext("Consider using special characters")}</li>
</ul>
</div>
<%= if @user do %>
<div class="mt-3 p-3 bg-orange-50 border border-orange-200 rounded">
<p class="text-sm text-orange-800">
<strong>{gettext("Admin Note")}:</strong> {gettext(
"As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
)}
</p>
</div>
<% end %>
</div>
<% else %>
<%= if @user do %>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-800">
<strong>{gettext("Note")}:</strong> {gettext(
"Check 'Change Password' above to set a new password for this user."
)}
</p>
</div>
<% else %>
<div class="mt-4 p-4 bg-yellow-50 rounded-lg">
<p class="text-sm text-yellow-800">
<strong>{gettext("Note")}:</strong> {gettext(
"User will be created without a password. Check 'Set Password' to add one."
)}
</p>
</div>
<% end %>
<% end %>
</div>
<.button phx-disable-with={gettext("Saving...")} variant="primary"> <.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save User")} {gettext("Save User")}
@ -56,6 +114,7 @@ defmodule MvWeb.UserLive.Form do
|> assign(:return_to, return_to(params["return_to"])) |> assign(:return_to, return_to(params["return_to"]))
|> assign(user: user) |> assign(user: user)
|> assign(:page_title, page_title) |> assign(:page_title, page_title)
|> assign(:show_password_fields, false)
|> assign_form()} |> assign_form()}
end end
@ -63,6 +122,19 @@ defmodule MvWeb.UserLive.Form do
defp return_to(_), do: "index" defp return_to(_), do: "index"
@impl true @impl true
def handle_event("toggle_password_section", _params, socket) do
show_password_fields = !socket.assigns.show_password_fields
socket =
socket
|> assign(:show_password_fields, show_password_fields)
|> assign_form()
{:noreply, socket}
end
def handle_event("validate", %{"user" => user_params}, socket) do def handle_event("validate", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))} {:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))}
end end
@ -86,12 +158,16 @@ defmodule MvWeb.UserLive.Form do
defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{user: user}} = socket) do defp assign_form(%{assigns: %{user: user, show_password_fields: show_password_fields}} = socket) do
form = form =
if user do if user do
AshPhoenix.Form.for_update(user, :update_user, domain: Mv.Accounts, as: "user") # For existing users, use admin password action if password fields are shown
action = if show_password_fields, do: :admin_set_password, else: :update_user
AshPhoenix.Form.for_update(user, action, domain: Mv.Accounts, as: "user")
else else
AshPhoenix.Form.for_create(Mv.Accounts.User, :create_user, # For new users, use password registration if password fields are shown
action = if show_password_fields, do: :register_with_password, else: :create_user
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
domain: Mv.Accounts, domain: Mv.Accounts,
as: "user" as: "user"
) )

View file

@ -502,3 +502,53 @@ msgstr "Alle Benutzer auswählen"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select user" msgid "Select user"
msgstr "Benutzer auswählen" msgstr "Benutzer auswählen"
#: lib/mv_web/live/user_live/form.ex:23
#, elixir-autogen, elixir-format
msgid "Set Password"
msgstr "Passwort setzen"
#: lib/mv_web/live/user_live/form.ex:35
#, elixir-autogen, elixir-format
msgid "Password"
msgstr "Passwort"
#: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Confirm Password"
msgstr "Passwort bestätigen"
#: lib/mv_web/live/user_live/form.ex:48
#, elixir-autogen, elixir-format
msgid "Password requirements"
msgstr "Passwort-Anforderungen"
#: lib/mv_web/live/user_live/form.ex:50
#, elixir-autogen, elixir-format
msgid "At least 8 characters"
msgstr "Mindestens 8 Zeichen"
#: lib/mv_web/live/user_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Include both letters and numbers"
msgstr "Buchstaben und Zahlen verwenden"
#: lib/mv_web/live/user_live/form.ex:52
#, elixir-autogen, elixir-format
msgid "Consider using special characters"
msgstr "Sonderzeichen empfohlen"
#: lib/mv_web/live/user_live/form.ex:57
#, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
#: lib/mv_web/live/user_live/form.ex:75
#, elixir-autogen, elixir-format
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
msgstr "Als Administrator können Sie direkt ein neues Passwort für diesen Benutzer setzen, wobei das gleiche sichere Ash Authentication System verwendet wird."
#: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user."
msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diesen Benutzer zu setzen."

View file

@ -492,4 +492,54 @@ msgstr ""
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users created here will need to set their password through the authentication system." msgid "Users created here will need to set their password through the authentication system."
msgstr "" msgstr "Users created here will need to set their password through the authentication system."
#: lib/mv_web/live/user_live/form.ex:23
#, elixir-autogen, elixir-format
msgid "Set Password"
msgstr "Set Password"
#: lib/mv_web/live/user_live/form.ex:35
#, elixir-autogen, elixir-format
msgid "Password"
msgstr "Password"
#: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Confirm Password"
msgstr "Confirm Password"
#: lib/mv_web/live/user_live/form.ex:48
#, elixir-autogen, elixir-format
msgid "Password requirements"
msgstr "Password requirements"
#: lib/mv_web/live/user_live/form.ex:50
#, elixir-autogen, elixir-format
msgid "At least 8 characters"
msgstr "At least 8 characters"
#: lib/mv_web/live/user_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Include both letters and numbers"
msgstr "Include both letters and numbers"
#: lib/mv_web/live/user_live/form.ex:52
#, elixir-autogen, elixir-format
msgid "Consider using special characters"
msgstr "Consider using special characters"
#: lib/mv_web/live/user_live/form.ex:57
#, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one."
#: lib/mv_web/live/user_live/form.ex:75
#, elixir-autogen, elixir-format
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
msgstr "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
#: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user."
msgstr "Check 'Change Password' above to set a new password for this user."