From 681db5dc71bf27692216947516dba1dedb1c0d22 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 17 Jul 2025 22:33:42 +0200 Subject: [PATCH 01/10] fix: set oidc_id from user_info["sub"] --- lib/accounts/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 0de4a38..18fd3aa 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -91,7 +91,7 @@ defmodule Mv.Accounts.User do changeset |> Ash.Changeset.change_attribute(:email, user_info["preferred_username"]) - |> Ash.Changeset.change_attribute(:oidc_id, user_info["id"]) + |> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"]) end end end -- 2.47.2 From fd8c853879b3bbb873eb1ecfd3af2118b50860d0 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 17 Jul 2025 21:25:44 +0200 Subject: [PATCH 02/10] feat: account live view - generated files --- lib/mv_web/live/user_live/form.ex | 80 ++++++++++++++++++++++++++++++ lib/mv_web/live/user_live/index.ex | 62 +++++++++++++++++++++++ lib/mv_web/live/user_live/show.ex | 38 ++++++++++++++ lib/mv_web/router.ex | 6 +++ 4 files changed, 186 insertions(+) create mode 100644 lib/mv_web/live/user_live/form.ex create mode 100644 lib/mv_web/live/user_live/index.ex create mode 100644 lib/mv_web/live/user_live/show.ex diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex new file mode 100644 index 0000000..53d7af7 --- /dev/null +++ b/lib/mv_web/live/user_live/form.ex @@ -0,0 +1,80 @@ +defmodule MvWeb.UserLive.Form do + use MvWeb, :live_view + + @impl true + def render(assigns) do + ~H""" + + <.header> + {@page_title} + <:subtitle>Use this form to manage user records in your database. + + + <.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> + <.button phx-disable-with="Saving..." variant="primary">Save User + <.button navigate={return_path(@return_to, @user)}>Cancel + + + """ + end + + @impl true + def mount(params, _session, socket) do + user = + case params["id"] do + nil -> nil + id -> Ash.get!(Mv.Accounts.User, id) + end + + action = if is_nil(user), do: "New", else: "Edit" + page_title = action <> " " <> "User" + + {:ok, + socket + |> assign(:return_to, return_to(params["return_to"])) + |> assign(user: user) + |> assign(:page_title, page_title) + |> assign_form()} + end + + defp return_to("show"), do: "show" + defp return_to(_), do: "index" + + @impl true + def handle_event("validate", %{"user" => user_params}, socket) do + {:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))} + end + + def handle_event("save", %{"user" => user_params}, socket) do + case AshPhoenix.Form.submit(socket.assigns.form, params: user_params) do + {:ok, user} -> + notify_parent({:saved, user}) + + socket = + socket + |> put_flash(:info, "User #{socket.assigns.form.source.type}d successfully") + |> push_navigate(to: return_path(socket.assigns.return_to, user)) + + {:noreply, socket} + + {:error, form} -> + {:noreply, assign(socket, form: form)} + end + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) + + defp assign_form(%{assigns: %{user: user}} = socket) do + form = + if user do + AshPhoenix.Form.for_update(user, :update, as: "user") + else + AshPhoenix.Form.for_create(Mv.Accounts.User, :create, as: "user") + end + + assign(socket, form: to_form(form)) + end + + defp return_path("index", _user), do: ~p"/users" + defp return_path("show", user), do: ~p"/users/#{user.id}" +end diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex new file mode 100644 index 0000000..47fee2f --- /dev/null +++ b/lib/mv_web/live/user_live/index.ex @@ -0,0 +1,62 @@ +defmodule MvWeb.UserLive.Index do + use MvWeb, :live_view + + @impl true + def render(assigns) do + ~H""" + + <.header> + Listing Users + <:actions> + <.button variant="primary" navigate={~p"/users/new"}> + <.icon name="hero-plus" /> New User + + + + + <.table + id="users" + rows={@streams.users} + row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end} + > + <:col :let={{_id, user}} label="Id">{user.id} + + <:col :let={{_id, user}} label="Email">{user.email} + + <:action :let={{_id, user}}> +
+ <.link navigate={~p"/users/#{user}"}>Show +
+ + <.link navigate={~p"/users/#{user}/edit"}>Edit + + + <:action :let={{id, user}}> + <.link + phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + +
+ """ + end + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, "Listing Users") + |> stream(:users, Ash.read!(Mv.Accounts.User))} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + user = Ash.get!(Mv.Accounts.User, id) + Ash.destroy!(user) + + {:noreply, stream_delete(socket, :users, user)} + end +end diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex new file mode 100644 index 0000000..f2fa602 --- /dev/null +++ b/lib/mv_web/live/user_live/show.ex @@ -0,0 +1,38 @@ +defmodule MvWeb.UserLive.Show do + use MvWeb, :live_view + + @impl true + def render(assigns) do + ~H""" + + <.header> + User {@user.id} + <:subtitle>This is a user record from your database. + + <:actions> + <.button navigate={~p"/users"}> + <.icon name="hero-arrow-left" /> + + <.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}> + <.icon name="hero-pencil-square" /> Edit User + + + + + <.list> + <:item title="Id">{@user.id} + + <:item title="Email">{@user.email} + + + """ + end + + @impl true + def mount(%{"id" => id}, _session, socket) do + {:ok, + socket + |> assign(:page_title, "Show User") + |> assign(:user, Ash.get!(Mv.Accounts.User, id))} + end +end diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index 75210b0..696a491 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -67,6 +67,12 @@ defmodule MvWeb.Router do live "/properties/:id", PropertyLive.Show, :show live "/properties/:id/show/edit", PropertyLive.Show, :edit + live "/users", UserLive.Index, :index + live "/users/new", UserLive.Form, :new + live "/users/:id/edit", UserLive.Form, :edit + live "/users/:id", UserLive.Show, :show + live "/users/:id/show/edit", UserLive.Show, :edit + post "/set_locale", LocaleController, :set_locale end -- 2.47.2 From df9966bb123ae9c500fee63a1c8b7eb5bb0e73ba Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 17 Jul 2025 22:07:44 +0200 Subject: [PATCH 03/10] feat: account live view - basic functionality --- lib/accounts/accounts.ex | 4 +- lib/accounts/user.ex | 8 ++ lib/mv_web/live/user_live/form.ex | 41 +++++++-- lib/mv_web/live/user_live/index.ex | 25 +++--- lib/mv_web/live/user_live/show.ex | 19 +++-- mix.lock | 2 +- priv/gettext/de/LC_MESSAGES/default.po | 112 ++++++++++++++++++++++++- priv/gettext/default.pot | 110 +++++++++++++++++++++++- priv/gettext/en/LC_MESSAGES/default.po | 110 +++++++++++++++++++++++- 9 files changed, 389 insertions(+), 42 deletions(-) diff --git a/lib/accounts/accounts.ex b/lib/accounts/accounts.ex index 756c468..333e12e 100644 --- a/lib/accounts/accounts.ex +++ b/lib/accounts/accounts.ex @@ -11,9 +11,9 @@ defmodule Mv.Accounts do resources do resource Mv.Accounts.User do - define :create_user, action: :create + define :create_user, action: :create_user define :list_users, action: :read - define :update_user, action: :update + define :update_user, action: :update_user define :destroy_user, action: :destroy end diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 18fd3aa..53156b4 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -63,6 +63,14 @@ defmodule Mv.Accounts.User do actions do defaults [:read, :create, :destroy, :update] + create :create_user do + accept [:email] + end + + update :update_user do + accept [:email] + end + read :get_by_subject do description "Get a user by the subject claim in a JWT" argument :subject, :string, allow_nil?: false diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index 53d7af7..dd78c0c 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -7,12 +7,34 @@ defmodule MvWeb.UserLive.Form do <.header> {@page_title} - <:subtitle>Use this form to manage user records in your database. + <:subtitle>{gettext("Use this form to manage user records in your database.")} <.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> - <.button phx-disable-with="Saving..." variant="primary">Save User - <.button navigate={return_path(@return_to, @user)}>Cancel + <.input field={@form[:email]} label={gettext("Email")} required type="email" /> + + <%= if @user do %> +
+

+ {gettext("Note")}: {gettext( + "Password can only be changed through authentication functions." + )} +

+
+ <% else %> +
+

+ {gettext("Note")}: {gettext( + "Users created here will need to set their password through the authentication system." + )} +

+
+ <% end %> + + <.button phx-disable-with={gettext("Saving...")} variant="primary"> + {gettext("Save User")} + + <.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")}
""" @@ -23,11 +45,11 @@ defmodule MvWeb.UserLive.Form do user = case params["id"] do nil -> nil - id -> Ash.get!(Mv.Accounts.User, id) + id -> Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts) end - action = if is_nil(user), do: "New", else: "Edit" - page_title = action <> " " <> "User" + action = if is_nil(user), do: gettext("New"), else: gettext("Edit") + page_title = action <> " " <> gettext("User") {:ok, socket @@ -67,9 +89,12 @@ defmodule MvWeb.UserLive.Form do defp assign_form(%{assigns: %{user: user}} = socket) do form = if user do - AshPhoenix.Form.for_update(user, :update, as: "user") + AshPhoenix.Form.for_update(user, :update_user, domain: Mv.Accounts, as: "user") else - AshPhoenix.Form.for_create(Mv.Accounts.User, :create, as: "user") + AshPhoenix.Form.for_create(Mv.Accounts.User, :create_user, + domain: Mv.Accounts, + as: "user" + ) end assign(socket, form: to_form(form)) diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex index 47fee2f..d3c2dcd 100644 --- a/lib/mv_web/live/user_live/index.ex +++ b/lib/mv_web/live/user_live/index.ex @@ -6,10 +6,10 @@ defmodule MvWeb.UserLive.Index do ~H""" <.header> - Listing Users + {gettext("Listing Users")} <:actions> <.button variant="primary" navigate={~p"/users/new"}> - <.icon name="hero-plus" /> New User + <.icon name="hero-plus" /> {gettext("New User")} @@ -19,24 +19,23 @@ defmodule MvWeb.UserLive.Index do rows={@streams.users} row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end} > - <:col :let={{_id, user}} label="Id">{user.id} - - <:col :let={{_id, user}} label="Email">{user.email} + <:col :let={{_id, user}} label={gettext("Email")}>{user.email} + <:col :let={{_id, user}} label={gettext("OIDC ID")}>{user.oidc_id} <:action :let={{_id, user}}>
- <.link navigate={~p"/users/#{user}"}>Show + <.link navigate={~p"/users/#{user}"}>{gettext("Show")}
- <.link navigate={~p"/users/#{user}/edit"}>Edit + <.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")} <:action :let={{id, user}}> <.link phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")} - data-confirm="Are you sure?" + data-confirm={gettext("Are you sure?")} > - Delete + {gettext("Delete")} @@ -48,14 +47,14 @@ defmodule MvWeb.UserLive.Index do def mount(_params, _session, socket) do {:ok, socket - |> assign(:page_title, "Listing Users") - |> stream(:users, Ash.read!(Mv.Accounts.User))} + |> assign(:page_title, gettext("Listing Users")) + |> stream(:users, Ash.read!(Mv.Accounts.User, domain: Mv.Accounts))} end @impl true def handle_event("delete", %{"id" => id}, socket) do - user = Ash.get!(Mv.Accounts.User, id) - Ash.destroy!(user) + user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts) + Ash.destroy!(user, domain: Mv.Accounts) {:noreply, stream_delete(socket, :users, user)} end diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index f2fa602..3bf6baf 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -6,23 +6,26 @@ defmodule MvWeb.UserLive.Show do ~H""" <.header> - User {@user.id} - <:subtitle>This is a user record from your database. + {gettext("User")} {@user.email} + <:subtitle>{gettext("This is a user record from your database.")} <:actions> <.button navigate={~p"/users"}> <.icon name="hero-arrow-left" /> <.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}> - <.icon name="hero-pencil-square" /> Edit User + <.icon name="hero-pencil-square" /> {gettext("Edit User")} <.list> - <:item title="Id">{@user.id} - - <:item title="Email">{@user.email} + <:item title={gettext("ID")}>{@user.id} + <:item title={gettext("Email")}>{@user.email} + <:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")} + <:item title={gettext("Password Authentication")}> + {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} + """ @@ -32,7 +35,7 @@ defmodule MvWeb.UserLive.Show do def mount(%{"id" => id}, _session, socket) do {:ok, socket - |> assign(:page_title, "Show User") - |> assign(:user, Ash.get!(Mv.Accounts.User, id))} + |> assign(:page_title, gettext("Show User")) + |> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))} end end diff --git a/mix.lock b/mix.lock index 818a216..d182c90 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,7 @@ "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.10.4", "b5a8e852dd48d875fe3089c28765379d112efed8bc1a5379f47e184d50259b73", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, ">= 4.9.1 and < 5.0.0-0", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "99e8ebc0606dc3ff81aac649c2838bf0d5d1d4bd880eb977cf31d2494b7a3f6a"}, "ash_phoenix": {:hex, :ash_phoenix, "2.3.11", "3003cb7fbda4eba829a67a064af15bc23f4032b86c768c3336e3a04218d19f37", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "33294de690af8c408759c629fd4212e1b4639b1da8417f5c7b31cb3898e6cb4f"}, "ash_postgres": {:hex, :ash_postgres, "2.6.11", "7c6b4fa9b8725c6644dd863323f8a2dae93f5df8ddae4b53df5dabde451a8b0c", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.72 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.14 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "d915f06406b46130559481b8b4290fd3bf60b0bd57bf5a4903cde0613b642c36"}, - "ash_sql": {:hex, :ash_sql, "0.2.87", "17197c643918cdaee657946a1998860402dcf53a980f7665bb81d1fa53c224e7", [], [{:ash, ">= 3.5.25 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "f82d6bf78f08bd9040af3adc28676965421598c88866074d8b1ccca65978d774"}, + "ash_sql": {:hex, :ash_sql, "0.2.87", "17197c643918cdaee657946a1998860402dcf53a980f7665bb81d1fa53c224e7", [:mix], [{:ash, ">= 3.5.25 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "f82d6bf78f08bd9040af3adc28676965421598c88866074d8b1ccca65978d774"}, "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, "bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index c3d4c84..2f36e27 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -16,6 +16,7 @@ msgid "Actions" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:77 +#: lib/mv_web/live/user_live/index.ex:36 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "Bist du sicher?" @@ -34,14 +35,17 @@ msgid "City" msgstr "Stadt" #: lib/mv_web/live/member_live/index.html.heex:79 +#: lib/mv_web/live/user_live/index.ex:38 #, elixir-autogen, elixir-format msgid "Delete" msgstr "Löschen" #: lib/mv_web/live/member_live/index.html.heex:71 +#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/index.ex:30 #, elixir-autogen, elixir-format msgid "Edit" -msgstr "Bearbeiten" +msgstr "Bearbeite" #: lib/mv_web/live/member_live/show.ex:18 #: lib/mv_web/live/member_live/show.ex:81 @@ -52,6 +56,9 @@ msgstr "Mitglied bearbeiten" #: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/show.ex:27 +#: lib/mv_web/live/user_live/form.ex:14 +#: lib/mv_web/live/user_live/index.ex:22 +#: lib/mv_web/live/user_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Email" msgstr "E-Mail" @@ -81,6 +88,7 @@ msgid "New Member" msgstr "Neues Mitglied" #: lib/mv_web/live/member_live/index.html.heex:68 +#: lib/mv_web/live/user_live/index.ex:27 #, elixir-autogen, elixir-format msgid "Show" msgstr "Anzeigen" @@ -159,6 +167,7 @@ msgstr "Mitglied speichern" #: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/property_type_live/form.ex:29 +#: lib/mv_web/live/user_live/form.ex:34 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "Speichern..." @@ -252,6 +261,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/property_type_live/form.ex:32 +#: lib/mv_web/live/user_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" @@ -271,16 +281,37 @@ msgstr "" msgid "Description" msgstr "" +#: lib/mv_web/live/user_live/show.ex:17 +#, elixir-autogen, elixir-format +msgid "Edit User" +msgstr "Benutzer bearbeiten" + +#: lib/mv_web/live/user_live/show.ex:27 +#, elixir-autogen, elixir-format +msgid "Enabled" +msgstr "Aktiviert" + +#: lib/mv_web/live/user_live/show.ex:23 +#, elixir-autogen, elixir-format +msgid "ID" +msgstr "ID" + #: lib/mv_web/live/property_type_live/form.ex:26 #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:35 +#: lib/mv_web/components/layouts/navbar.ex:73 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" +#: lib/mv_web/live/user_live/index.ex:9 +#: lib/mv_web/live/user_live/index.ex:50 +#, elixir-autogen, elixir-format +msgid "Listing Users" +msgstr "Benutzer auflisten" + #: lib/mv_web/live/property_live/form.ex:27 #, elixir-autogen, elixir-format msgid "Member" @@ -299,12 +330,49 @@ msgstr "Mitglieder" msgid "Name" msgstr "" +#: lib/mv_web/live/user_live/index.ex:12 +#, elixir-autogen, elixir-format +msgid "New User" +msgstr "Neuer Benutzer" + +#: lib/mv_web/live/user_live/show.ex:27 +#, elixir-autogen, elixir-format +msgid "Not enabled" +msgstr "Nicht aktiviert" + +#: lib/mv_web/live/user_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "Not set" +msgstr "Nicht gesetzt" + +#: lib/mv_web/live/user_live/form.ex:19 +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format +msgid "Note" +msgstr "Hinweis" + +#: lib/mv_web/live/user_live/index.ex:23 +#: lib/mv_web/live/user_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "OIDC ID" +msgstr "OIDC ID" + +#: lib/mv_web/live/user_live/show.ex:26 +#, elixir-autogen, elixir-format +msgid "Password Authentication" +msgstr "Passwort-Authentifizierung" + +#: lib/mv_web/live/user_live/form.ex:19 +#, elixir-autogen, elixir-format +msgid "Password can only be changed through authentication functions." +msgstr "" + #: lib/mv_web/live/property_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Please select a property type first" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:31 +#: lib/mv_web/components/layouts/navbar.ex:69 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -349,11 +417,26 @@ msgstr "" msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:34 +#: lib/mv_web/components/layouts/navbar.ex:72 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" +#: lib/mv_web/live/user_live/form.ex:35 +#, elixir-autogen, elixir-format +msgid "Save User" +msgstr "Benutzer speichern" + +#: lib/mv_web/live/user_live/show.ex:38 +#, elixir-autogen, elixir-format +msgid "Show User" +msgstr "Benutzer anzeigen" + +#: lib/mv_web/live/user_live/show.ex:10 +#, elixir-autogen, elixir-format +msgid "This is a user record from your database." +msgstr "Dies ist ein Benutzer-Datensatz aus Ihrer Datenbank." + #: lib/mv_web/live/property_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Unsupported value type: %{type}" @@ -369,6 +452,17 @@ msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenscha msgid "Use this form to manage property_type records in your database." msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." +#: lib/mv_web/live/user_live/form.ex:10 +#, elixir-autogen, elixir-format +msgid "Use this form to manage user records in your database." +msgstr "Verwenden Sie dieses Formular, um Benutzer-Datensätze zu verwalten." + +#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/show.ex:9 +#, elixir-autogen, elixir-format +msgid "User" +msgstr "Benutzer" + #: lib/mv_web/live/property_live/form.ex:59 #, elixir-autogen, elixir-format msgid "Value" @@ -388,3 +482,13 @@ msgstr "" #, elixir-autogen, elixir-format msgid "descending" msgstr "" + +#: lib/mv_web/live/user_live/form.ex:51 +#, elixir-autogen, elixir-format +msgid "New" +msgstr "Neuer" + +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format +msgid "Users created here will need to set their password through the authentication system." +msgstr "Hier erstellte Benutzer müssen ihr Passwort über das Authentifizierungssystem setzen." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index bb17d15..baa059a 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -17,6 +17,7 @@ msgid "Actions" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:77 +#: lib/mv_web/live/user_live/index.ex:36 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" @@ -35,11 +36,14 @@ msgid "City" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:79 +#: lib/mv_web/live/user_live/index.ex:38 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:71 +#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/index.ex:30 #, elixir-autogen, elixir-format msgid "Edit" msgstr "" @@ -53,6 +57,9 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/show.ex:27 +#: lib/mv_web/live/user_live/form.ex:14 +#: lib/mv_web/live/user_live/index.ex:22 +#: lib/mv_web/live/user_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Email" msgstr "" @@ -82,6 +89,7 @@ msgid "New Member" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:68 +#: lib/mv_web/live/user_live/index.ex:27 #, elixir-autogen, elixir-format msgid "Show" msgstr "" @@ -160,6 +168,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/property_type_live/form.ex:29 +#: lib/mv_web/live/user_live/form.ex:34 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "" @@ -253,6 +262,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/property_type_live/form.ex:32 +#: lib/mv_web/live/user_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" @@ -272,16 +282,37 @@ msgstr "" msgid "Description" msgstr "" +#: lib/mv_web/live/user_live/show.ex:17 +#, elixir-autogen, elixir-format +msgid "Edit User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:27 +#, elixir-autogen, elixir-format +msgid "Enabled" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:23 +#, elixir-autogen, elixir-format +msgid "ID" +msgstr "" + #: lib/mv_web/live/property_type_live/form.ex:26 #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:35 +#: lib/mv_web/components/layouts/navbar.ex:73 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" +#: lib/mv_web/live/user_live/index.ex:9 +#: lib/mv_web/live/user_live/index.ex:50 +#, elixir-autogen, elixir-format +msgid "Listing Users" +msgstr "" + #: lib/mv_web/live/property_live/form.ex:27 #, elixir-autogen, elixir-format msgid "Member" @@ -300,12 +331,49 @@ msgstr "" msgid "Name" msgstr "" +#: lib/mv_web/live/user_live/index.ex:12 +#, elixir-autogen, elixir-format +msgid "New User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:27 +#, elixir-autogen, elixir-format +msgid "Not enabled" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "Not set" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:19 +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format +msgid "Note" +msgstr "" + +#: lib/mv_web/live/user_live/index.ex:23 +#: lib/mv_web/live/user_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "OIDC ID" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:26 +#, elixir-autogen, elixir-format +msgid "Password Authentication" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:19 +#, elixir-autogen, elixir-format +msgid "Password can only be changed through authentication functions." +msgstr "" + #: lib/mv_web/live/property_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Please select a property type first" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:31 +#: lib/mv_web/components/layouts/navbar.ex:69 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -350,11 +418,26 @@ msgstr "" msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:34 +#: lib/mv_web/components/layouts/navbar.ex:72 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" +#: lib/mv_web/live/user_live/form.ex:35 +#, elixir-autogen, elixir-format +msgid "Save User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:38 +#, elixir-autogen, elixir-format +msgid "Show User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:10 +#, elixir-autogen, elixir-format +msgid "This is a user record from your database." +msgstr "" + #: lib/mv_web/live/property_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Unsupported value type: %{type}" @@ -370,6 +453,17 @@ msgstr "" msgid "Use this form to manage property_type records in your database." msgstr "" +#: lib/mv_web/live/user_live/form.ex:10 +#, elixir-autogen, elixir-format +msgid "Use this form to manage user records in your database." +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/show.ex:9 +#, elixir-autogen, elixir-format +msgid "User" +msgstr "" + #: lib/mv_web/live/property_live/form.ex:59 #, elixir-autogen, elixir-format msgid "Value" @@ -389,3 +483,13 @@ msgstr "" #, elixir-autogen, elixir-format msgid "descending" msgstr "" + +#: lib/mv_web/live/user_live/form.ex:51 +#, elixir-autogen, elixir-format +msgid "New" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format +msgid "Users created here will need to set their password through the authentication system." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 616b323..4bddf9a 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -17,6 +17,7 @@ msgid "Actions" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:77 +#: lib/mv_web/live/user_live/index.ex:36 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" @@ -35,11 +36,14 @@ msgid "City" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:79 +#: lib/mv_web/live/user_live/index.ex:38 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:71 +#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/index.ex:30 #, elixir-autogen, elixir-format msgid "Edit" msgstr "" @@ -53,6 +57,9 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/show.ex:27 +#: lib/mv_web/live/user_live/form.ex:14 +#: lib/mv_web/live/user_live/index.ex:22 +#: lib/mv_web/live/user_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Email" msgstr "" @@ -82,6 +89,7 @@ msgid "New Member" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:68 +#: lib/mv_web/live/user_live/index.ex:27 #, elixir-autogen, elixir-format msgid "Show" msgstr "" @@ -160,6 +168,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/property_type_live/form.ex:29 +#: lib/mv_web/live/user_live/form.ex:34 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "" @@ -253,6 +262,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/property_type_live/form.ex:32 +#: lib/mv_web/live/user_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" @@ -272,16 +282,37 @@ msgstr "" msgid "Description" msgstr "" +#: lib/mv_web/live/user_live/show.ex:17 +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:27 +#, elixir-autogen, elixir-format +msgid "Enabled" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:23 +#, elixir-autogen, elixir-format +msgid "ID" +msgstr "" + #: lib/mv_web/live/property_type_live/form.ex:26 #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:35 +#: lib/mv_web/components/layouts/navbar.ex:73 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" +#: lib/mv_web/live/user_live/index.ex:9 +#: lib/mv_web/live/user_live/index.ex:50 +#, elixir-autogen, elixir-format, fuzzy +msgid "Listing Users" +msgstr "" + #: lib/mv_web/live/property_live/form.ex:27 #, elixir-autogen, elixir-format msgid "Member" @@ -300,12 +331,49 @@ msgstr "" msgid "Name" msgstr "" +#: lib/mv_web/live/user_live/index.ex:12 +#, elixir-autogen, elixir-format +msgid "New User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:27 +#, elixir-autogen, elixir-format +msgid "Not enabled" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:25 +#, elixir-autogen, elixir-format, fuzzy +msgid "Not set" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:19 +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format, fuzzy +msgid "Note" +msgstr "" + +#: lib/mv_web/live/user_live/index.ex:23 +#: lib/mv_web/live/user_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "OIDC ID" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:26 +#, elixir-autogen, elixir-format +msgid "Password Authentication" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:19 +#, elixir-autogen, elixir-format +msgid "Password can only be changed through authentication functions." +msgstr "" + #: lib/mv_web/live/property_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Please select a property type first" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:31 +#: lib/mv_web/components/layouts/navbar.ex:69 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -350,11 +418,26 @@ msgstr "" msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:34 +#: lib/mv_web/components/layouts/navbar.ex:72 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" +#: lib/mv_web/live/user_live/form.ex:35 +#, elixir-autogen, elixir-format, fuzzy +msgid "Save User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:38 +#, elixir-autogen, elixir-format, fuzzy +msgid "Show User" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex:10 +#, elixir-autogen, elixir-format, fuzzy +msgid "This is a user record from your database." +msgstr "" + #: lib/mv_web/live/property_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Unsupported value type: %{type}" @@ -370,6 +453,17 @@ msgstr "" msgid "Use this form to manage property_type records in your database." msgstr "" +#: lib/mv_web/live/user_live/form.ex:10 +#, elixir-autogen, elixir-format, fuzzy +msgid "Use this form to manage user records in your database." +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/show.ex:9 +#, elixir-autogen, elixir-format +msgid "User" +msgstr "" + #: lib/mv_web/live/property_live/form.ex:59 #, elixir-autogen, elixir-format msgid "Value" @@ -389,3 +483,13 @@ msgstr "" #, elixir-autogen, elixir-format msgid "descending" msgstr "" + +#: lib/mv_web/live/user_live/form.ex:51 +#, elixir-autogen, elixir-format +msgid "New" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format +msgid "Users created here will need to set their password through the authentication system." +msgstr "" -- 2.47.2 From 5959c9f5453af2784b189fc467e2816255278840 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 22 Jul 2025 19:57:18 +0200 Subject: [PATCH 04/10] feat: use layout from memberlist --- lib/mv_web/live/user_live/index.ex | 113 +++++++++++++--------- lib/mv_web/live/user_live/index.html.heex | 75 ++++++++++++++ priv/gettext/de/LC_MESSAGES/default.po | 18 +++- 3 files changed, 158 insertions(+), 48 deletions(-) create mode 100644 lib/mv_web/live/user_live/index.html.heex diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex index d3c2dcd..39ced23 100644 --- a/lib/mv_web/live/user_live/index.ex +++ b/lib/mv_web/live/user_live/index.ex @@ -1,54 +1,19 @@ defmodule MvWeb.UserLive.Index do use MvWeb, :live_view - - @impl true - def render(assigns) do - ~H""" - - <.header> - {gettext("Listing Users")} - <:actions> - <.button variant="primary" navigate={~p"/users/new"}> - <.icon name="hero-plus" /> {gettext("New User")} - - - - - <.table - id="users" - rows={@streams.users} - row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end} - > - <:col :let={{_id, user}} label={gettext("Email")}>{user.email} - <:col :let={{_id, user}} label={gettext("OIDC ID")}>{user.oidc_id} - - <:action :let={{_id, user}}> -
- <.link navigate={~p"/users/#{user}"}>{gettext("Show")} -
- - <.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")} - - - <:action :let={{id, user}}> - <.link - phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")} - data-confirm={gettext("Are you sure?")} - > - {gettext("Delete")} - - - -
- """ - end + import MvWeb.TableComponents @impl true def mount(_params, _session, socket) do + users = Ash.read!(Mv.Accounts.User, domain: Mv.Accounts) + sorted = Enum.sort_by(users, & &1.email) + {:ok, socket |> assign(:page_title, gettext("Listing Users")) - |> stream(:users, Ash.read!(Mv.Accounts.User, domain: Mv.Accounts))} + |> assign(:sort_field, :email) + |> assign(:sort_order, :asc) + |> assign(:users, sorted) + |> assign(:selected_users, [])} end @impl true @@ -56,6 +21,66 @@ defmodule MvWeb.UserLive.Index do user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts) Ash.destroy!(user, domain: Mv.Accounts) - {:noreply, stream_delete(socket, :users, user)} + updated_users = Enum.reject(socket.assigns.users, &(&1.id == id)) + {:noreply, assign(socket, :users, updated_users)} end + + # Selects one user in the list of users + @impl true + def handle_event("select_user", %{"id" => id}, socket) do + selected = + if id in socket.assigns.selected_users do + List.delete(socket.assigns.selected_users, id) + else + [id | socket.assigns.selected_users] + end + + {:noreply, assign(socket, :selected_users, selected)} + end + + # Sorts the list of users according to a field, when you click on the column header + @impl true + def handle_event("sort", %{"field" => field_str}, socket) do + users = socket.assigns.users + field = String.to_existing_atom(field_str) + + new_order = + if socket.assigns.sort_field == field do + toggle_order(socket.assigns.sort_order) + else + :asc + end + + sorted_users = + users + |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order)) + + {:noreply, + socket + |> assign(:sort_field, field) + |> assign(:sort_order, new_order) + |> assign(:users, sorted_users)} + end + + # Selects all users in the list of users + @impl true + def handle_event("select_all", _params, socket) do + users = socket.assigns.users + + all_ids = Enum.map(users, & &1.id) + + selected = + if Enum.sort(socket.assigns.selected_users) == Enum.sort(all_ids) do + [] + else + all_ids + end + + {:noreply, assign(socket, :selected_users, selected)} + end + + defp toggle_order(:asc), do: :desc + defp toggle_order(:desc), do: :asc + defp sort_fun(:asc), do: &<=/2 + defp sort_fun(:desc), do: &>=/2 end diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex new file mode 100644 index 0000000..5b313f0 --- /dev/null +++ b/lib/mv_web/live/user_live/index.html.heex @@ -0,0 +1,75 @@ + + <.header> + {gettext("Listing Users")} + <:actions> + <.button variant="primary" navigate={~p"/users/new"}> + <.icon name="hero-plus" /> {gettext("New User")} + + + + + <.table + id="users" + rows={@users} + row_click={fn user -> JS.navigate(~p"/users/#{user}") end} + > + <:col + :let={user} + label={ + ~H""" + <.input + type="checkbox" + name="select_all" + phx-click="select_all" + checked={Enum.sort(@selected_users) == Enum.map(@users, & &1.id) |> Enum.sort()} + aria-label={gettext("Select all users")} + role="checkbox" + /> + """ + } + > + <.input + type="checkbox" + name={user.id} + phx-click="select_user" + phx-value-id={user.id} + checked={user.id in @selected_users} + phx-capture-click + phx-stop-propagation + aria-label={gettext("Select user")} + role="checkbox" + /> + + <:col + :let={user} + label={ + sort_button(%{ + field: :email, + label: gettext("Email"), + sort_field: @sort_field, + sort_order: @sort_order + }) + } + > + {user.email} + + <:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id} + + <:action :let={user}> +
+ <.link navigate={~p"/users/#{user}"}>{gettext("Show")} +
+ + <.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")} + + + <:action :let={user}> + <.link + phx-click={JS.push("delete", value: %{id: user.id}) |> hide("#row-#{user.id}")} + data-confirm={gettext("Are you sure?")} + > + {gettext("Delete")} + + + +
\ No newline at end of file diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 2f36e27..d466469 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -410,12 +410,12 @@ msgstr "" #: lib/mv_web/live/member_live/index.html.heex:27 #, elixir-autogen, elixir-format msgid "Select all members" -msgstr "" +msgstr "Alle Mitglieder auswählen" #: lib/mv_web/live/member_live/index.html.heex:41 #, elixir-autogen, elixir-format msgid "Select member" -msgstr "" +msgstr "Mitglied auswählen" #: lib/mv_web/components/layouts/navbar.ex:72 #, elixir-autogen, elixir-format @@ -476,12 +476,12 @@ msgstr "" #: lib/mv_web/components/table_components.ex:30 #, elixir-autogen, elixir-format msgid "ascending" -msgstr "" +msgstr "aufsteigend" #: lib/mv_web/components/table_components.ex:30 #, elixir-autogen, elixir-format msgid "descending" -msgstr "" +msgstr "absteigend" #: lib/mv_web/live/user_live/form.ex:51 #, elixir-autogen, elixir-format @@ -492,3 +492,13 @@ msgstr "Neuer" #, elixir-autogen, elixir-format msgid "Users created here will need to set their password through the authentication system." msgstr "Hier erstellte Benutzer müssen ihr Passwort über das Authentifizierungssystem setzen." + +#: lib/mv_web/live/user_live/index.html.heex:15 +#, elixir-autogen, elixir-format +msgid "Select all users" +msgstr "Alle Benutzer auswählen" + +#: lib/mv_web/live/user_live/index.html.heex:29 +#, elixir-autogen, elixir-format +msgid "Select user" +msgstr "Benutzer auswählen" -- 2.47.2 From 2e256a020624409685d0e0c6079605f3d3eba211 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 22 Jul 2025 21:21:11 +0200 Subject: [PATCH 05/10] feat: add user view tests --- test/mv_web/user_live/index_test.exs | 375 +++++++++++++++++++++++++++ test/support/conn_case.ex | 55 +++- 2 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 test/mv_web/user_live/index_test.exs diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs new file mode 100644 index 0000000..ac4e65a --- /dev/null +++ b/test/mv_web/user_live/index_test.exs @@ -0,0 +1,375 @@ +defmodule MvWeb.UserLive.IndexTest do + use MvWeb.ConnCase, async: true + import Phoenix.LiveViewTest + + describe "basic functionality" do + test "shows translated title in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = Plug.Test.init_test_session(conn, locale: "de") + {:ok, _view, html} = live(conn, "/users") + assert html =~ "Benutzer auflisten" + end + + test "shows translated title in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) + Gettext.put_locale(MvWeb.Gettext, "en") + {:ok, _view, html} = live(conn, "/users") + assert html =~ "Listing Users" + end + + test "shows New User button", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + assert html =~ "New User" + end + + test "displays users in a table", %{conn: conn} do + # Create test users + _user1 = create_test_user(%{email: "alice@example.com", oidc_id: "alice123"}) + _user2 = create_test_user(%{email: "bob@example.com", oidc_id: "bob456"}) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ "alice@example.com" + assert html =~ "bob@example.com" + assert html =~ "alice123" + assert html =~ "bob456" + end + + test "shows correct action links", %{conn: conn} do + user = create_test_user(%{email: "test@example.com"}) + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ "Edit" + assert html =~ "Delete" + assert html =~ ~r/href="[^"]*\/users\/#{user.id}\/edit"/ + end + end + + describe "sorting functionality" do + setup do + # Create users with different emails for sorting tests + user_a = create_test_user(%{email: "alpha@example.com", oidc_id: "alpha"}) + user_z = create_test_user(%{email: "zulu@example.com", oidc_id: "zulu"}) + user_m = create_test_user(%{email: "mike@example.com", oidc_id: "mike"}) + + %{users: [user_a, user_z, user_m]} + end + + test "initially sorts by email ascending", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + # Should show ascending indicator (up arrow) + assert html =~ "hero-chevron-up" + assert html =~ ~s(aria-sort="ascending") + + # Test actual sort order: alpha should appear before mike, mike before zulu + alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0) + mike_pos = html |> :binary.match("mike@example.com") |> elem(0) + zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0) + + assert alpha_pos < mike_pos, "alpha@example.com should appear before mike@example.com" + assert mike_pos < zulu_pos, "mike@example.com should appear before zulu@example.com" + end + + test "can sort email descending by clicking sort button", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Click on email sort button and get rendered result + html = view |> element("button[phx-value-field='email']") |> render_click() + + # Should now show descending indicator (down arrow) + assert html =~ "hero-chevron-down" + assert html =~ ~s(aria-sort="descending") + + # Test actual sort order reversed: zulu should now appear before mike, mike before alpha + alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0) + mike_pos = html |> :binary.match("mike@example.com") |> elem(0) + zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0) + + assert zulu_pos < mike_pos, "zulu@example.com should appear before mike@example.com when sorted desc" + assert mike_pos < alpha_pos, "mike@example.com should appear before alpha@example.com when sorted desc" + end + + test "toggles back to ascending when clicking sort button twice", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Click twice to toggle: asc -> desc -> asc + view |> element("button[phx-value-field='email']") |> render_click() + html = view |> element("button[phx-value-field='email']") |> render_click() + + # Should be back to ascending + assert html =~ "hero-chevron-up" + assert html =~ ~s(aria-sort="ascending") + + # Should be back to original ascending order + alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0) + mike_pos = html |> :binary.match("mike@example.com") |> elem(0) + zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0) + + assert alpha_pos < mike_pos, "Should be back to ascending: alpha before mike" + assert mike_pos < zulu_pos, "Should be back to ascending: mike before zulu" + end + + test "shows sort direction icons", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Initially ascending - should show up arrow + html = render(view) + assert html =~ "hero-chevron-up" + + # After clicking, should show down arrow + view |> element("button[phx-value-field='email']") |> render_click() + html = render(view) + assert html =~ "hero-chevron-down" + end + end + + describe "checkbox selection functionality" do + setup do + user1 = create_test_user(%{email: "user1@example.com", oidc_id: "user1"}) + user2 = create_test_user(%{email: "user2@example.com", oidc_id: "user2"}) + %{users: [user1, user2]} + end + + test "shows select all checkbox", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ ~s(name="select_all") + assert html =~ ~s(phx-click="select_all") + end + + test "shows individual user checkboxes", %{conn: conn, users: [user1, user2]} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ ~s(name="#{user1.id}") + assert html =~ ~s(name="#{user2.id}") + assert html =~ ~s(phx-click="select_user") + end + + test "can select individual users", %{conn: conn, users: [user1, user2]} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Initially, individual checkboxes should exist but not be checked + assert view |> element("input[type='checkbox'][name='#{user1.id}']") |> has_element?() + assert view |> element("input[type='checkbox'][name='#{user2.id}']") |> has_element?() + + # Initially, select_all should not be checked (since no individual items are selected) + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Select first user checkbox + html = view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() + + # The select_all checkbox should still not be checked (not all users selected) + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Page should still function normally + assert html =~ "Email" + assert html =~ to_string(user1.email) + end + + test "can deselect individual users", %{conn: conn, users: [user1, _user2]} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Select user first + view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() + + # Then deselect user + html = view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() + + # Select all should not be checked after deselecting individual user + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Page should still function normally + assert html =~ "Email" + assert html =~ to_string(user1.email) + end + + test "select all functionality selects all users", %{conn: conn, users: [user1, user2]} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Initially no checkboxes should be checked + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + refute view |> element("input[type='checkbox'][name='#{user1.id}'][checked]") |> has_element?() + refute view |> element("input[type='checkbox'][name='#{user2.id}'][checked]") |> has_element?() + + # Click select all + html = view |> element("input[type='checkbox'][name='select_all']") |> render_click() + + # After selecting all, the select_all checkbox should be checked + assert view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Page should still function normally and show all users + assert html =~ "Email" + assert html =~ to_string(user1.email) + assert html =~ to_string(user2.email) + end + + test "deselect all functionality deselects all users", %{conn: conn, users: [user1, user2]} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Select all first + view |> element("input[type='checkbox'][name='select_all']") |> render_click() + + # Verify that select_all is checked + assert view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Then deselect all + html = view |> element("input[type='checkbox'][name='select_all']") |> render_click() + + # After deselecting all, no checkboxes should be checked + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + refute view |> element("input[type='checkbox'][name='#{user1.id}'][checked]") |> has_element?() + refute view |> element("input[type='checkbox'][name='#{user2.id}'][checked]") |> has_element?() + + # Page should still function normally + assert html =~ "Email" + assert html =~ to_string(user1.email) + assert html =~ to_string(user2.email) + end + + test "select all automatically checks when all individual users are selected", %{conn: conn, users: [user1, user2]} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Initially nothing should be checked + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Select first user + view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() + # Select all should still not be checked (only 1 of 2+ users selected) + refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + + # Select second user + html = view |> element("input[type='checkbox'][name='#{user2.id}']") |> render_click() + + # Now select all should be automatically checked (all individual users are selected) + # Note: This test might need adjustment based on actual implementation + # The logic depends on whether authenticated user is included in the count + assert html =~ "Email" + assert html =~ to_string(user1.email) + assert html =~ to_string(user2.email) + end + end + + describe "delete functionality" do + test "can delete a user", %{conn: conn} do + _user = create_test_user(%{email: "delete-me@example.com"}) + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # Confirm user is displayed + assert render(view) =~ "delete-me@example.com" + + # Click the first delete button to test the functionality + view |> element("tbody tr:first-child a[data-confirm]") |> render_click() + + # The page should still render (basic functionality test) + html = render(view) + assert html =~ "Email" # Table header should still be there + end + + test "shows delete confirmation", %{conn: conn} do + _user = create_test_user(%{email: "confirm-delete@example.com"}) + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + # Check that delete link has confirmation attribute + assert html =~ ~s(data-confirm="Are you sure?") + end + end + + describe "navigation" do + test "clicking on user row navigates to user show page", %{conn: conn} do + user = create_test_user(%{email: "navigate@example.com"}) + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/users") + + # This test would need to check row click behavior + # The actual navigation would happen via JavaScript + html = render(view) + assert html =~ ~s(/users/#{user.id}) + end + + test "edit link points to correct edit page", %{conn: conn} do + user = create_test_user(%{email: "edit-me@example.com"}) + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ ~s(/users/#{user.id}/edit) + end + + test "new user button points to correct new page", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ ~s(/users/new) + end + end + + describe "translations" do + test "shows German translations for selection", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = Plug.Test.init_test_session(conn, locale: "de") + {:ok, _view, html} = live(conn, "/users") + + assert html =~ "Alle Benutzer auswählen" + assert html =~ "Benutzer auswählen" + end + + test "shows English translations for selection", %{conn: conn} do + conn = conn_with_oidc_user(conn) + Gettext.put_locale(MvWeb.Gettext, "en") + {:ok, _view, html} = live(conn, "/users") + + # Note: English translations might be empty strings by default + # This test would verify the structure is there + assert html =~ ~s(aria-label=) # Checking that aria-label attributes exist + end + end + + describe "edge cases" do + test "handles empty user list gracefully", %{conn: conn} do + # Don't create any users besides the authenticated one + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + # Should still show the table structure + assert html =~ "Email" + assert html =~ "OIDC ID" + # Should show the authenticated user at minimum + assert html =~ "user@example.com" + end + + test "handles users with missing OIDC ID", %{conn: conn} do + _user = create_test_user(%{email: "no-oidc@example.com", oidc_id: nil}) + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ "no-oidc@example.com" + # Should handle nil OIDC ID gracefully + end + + test "handles very long email addresses", %{conn: conn} do + long_email = "very.long.email.address.that.might.break.layouts@example.com" + _user = create_test_user(%{email: long_email}) + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/users") + + assert html =~ long_email + end + end + +end \ No newline at end of file diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index d1804b7..385083d 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -33,16 +33,54 @@ defmodule MvWeb.ConnCase do @doc """ Creates a test user and returns the user struct. + Accepts attrs to override default values. + + Password handling: + - If `hashed_password` is provided in attrs, it's used directly + - If `password` is provided in attrs, it gets hashed automatically + - If neither is provided, uses default password "password" + + ## Examples + + create_test_user() # Default user with unique email + create_test_user(%{email: "custom@example.com"}) # Custom email + create_test_user(%{password: "secret123"}) # Custom password (gets hashed) + create_test_user(%{hashed_password: "$2b$..."}) # Pre-hashed password """ def create_test_user(attrs \\ %{}) do - email = "user@example.com" - password = "password" - {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + # Generate unique values to avoid conflicts + unique_id = System.unique_integer([:positive]) + + default_attrs = %{ + email: "user#{unique_id}@example.com", + oidc_id: "oidc#{unique_id}" + } + + # Merge provided attrs with defaults + user_attrs = Map.merge(default_attrs, attrs) + + # Handle password/hashed_password + final_attrs = cond do + # If hashed_password is already provided, use it as-is + Map.has_key?(user_attrs, :hashed_password) -> + user_attrs + + # If password is provided, hash it + Map.has_key?(user_attrs, :password) -> + password = Map.get(user_attrs, :password) + {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + user_attrs + |> Map.delete(:password) # Remove plain password + |> Map.put(:hashed_password, hashed_password) + + # Neither provided, use default password + true -> + password = "password" + {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + Map.put(user_attrs, :hashed_password, hashed_password) + end - Ash.Seed.seed!(Mv.Accounts.User, %{ - email: email, - hashed_password: hashed_password - }) + Ash.Seed.seed!(Mv.Accounts.User, final_attrs) end @doc """ @@ -57,8 +95,9 @@ defmodule MvWeb.ConnCase do @doc """ Signs in a user via OIDC and returns a connection with the user authenticated. + By default creates a user with "user@example.com" for consistency. """ - def conn_with_oidc_user(conn, user_attrs \\ %{}) do + def conn_with_oidc_user(conn, user_attrs \\ %{email: "user@example.com"}) do user = create_test_user(user_attrs) sign_in_user_via_oidc(conn, user) end -- 2.47.2 From 662e80cc741bdbc8e814c2a455eb42f2854959e0 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 22 Jul 2025 22:12:43 +0200 Subject: [PATCH 06/10] feat: set password for new and for existing user --- lib/accounts/user.ex | 21 +++++ lib/mv_web/live/user_live/form.ex | 116 ++++++++++++++++++++----- priv/gettext/de/LC_MESSAGES/default.po | 50 +++++++++++ priv/gettext/en/LC_MESSAGES/default.po | 52 ++++++++++- 4 files changed, 218 insertions(+), 21 deletions(-) diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 53156b4..daf29ba 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -71,6 +71,19 @@ defmodule Mv.Accounts.User do accept [:email] 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 description "Get a user by the subject claim in a JWT" argument :subject, :string, allow_nil?: false @@ -121,6 +134,14 @@ defmodule Mv.Accounts.User do identity :unique_oidc_id, [:oidc_id] 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 # only allows user data to be interacted with via AshAuthentication. # policies do diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index dd78c0c..b599bc1 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -13,23 +13,81 @@ defmodule MvWeb.UserLive.Form do <.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.input field={@form[:email]} label={gettext("Email")} required type="email" /> - <%= if @user do %> -
-

- {gettext("Note")}: {gettext( - "Password can only be changed through authentication functions." - )} -

-
- <% else %> -
-

- {gettext("Note")}: {gettext( - "Users created here will need to set their password through the authentication system." - )} -

-
- <% end %> + +
+ + + <%= if @show_password_fields do %> +
+ <.input + field={@form[:password]} + label={gettext("Password")} + type="password" + required + autocomplete="new-password" + /> + + + <%= if !@user do %> + <.input + field={@form[:password_confirmation]} + label={gettext("Confirm Password")} + type="password" + required + autocomplete="new-password" + /> + <% end %> + +
+

{gettext("Password requirements")}:

+
    +
  • {gettext("At least 8 characters")}
  • +
  • {gettext("Include both letters and numbers")}
  • +
  • {gettext("Consider using special characters")}
  • +
+
+ + <%= if @user do %> +
+

+ {gettext("Admin Note")}: {gettext( + "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system." + )} +

+
+ <% end %> +
+ <% else %> + <%= if @user do %> +
+

+ {gettext("Note")}: {gettext( + "Check 'Change Password' above to set a new password for this user." + )} +

+
+ <% else %> +
+

+ {gettext("Note")}: {gettext( + "User will be created without a password. Check 'Set Password' to add one." + )} +

+
+ <% end %> + <% end %> +
<.button phx-disable-with={gettext("Saving...")} variant="primary"> {gettext("Save User")} @@ -56,6 +114,7 @@ defmodule MvWeb.UserLive.Form do |> assign(:return_to, return_to(params["return_to"])) |> assign(user: user) |> assign(:page_title, page_title) + |> assign(:show_password_fields, false) |> assign_form()} end @@ -63,6 +122,19 @@ defmodule MvWeb.UserLive.Form do defp return_to(_), do: "index" @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 {:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))} end @@ -86,12 +158,16 @@ defmodule MvWeb.UserLive.Form do 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 = 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 - 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, as: "user" ) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index d466469..30d8ad6 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -502,3 +502,53 @@ msgstr "Alle Benutzer auswählen" #, elixir-autogen, elixir-format msgid "Select user" 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." diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 4bddf9a..0536761 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -492,4 +492,54 @@ msgstr "" #: lib/mv_web/live/user_live/form.ex:27 #, elixir-autogen, elixir-format 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." -- 2.47.2 From a09e2add2f8083fd35abed89d5cf216a089fdb0d Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 22 Jul 2025 22:48:33 +0200 Subject: [PATCH 07/10] feat: add missing translation --- priv/gettext/de/LC_MESSAGES/default.po | 170 ++++++++++++------------- priv/gettext/default.pot | 106 +++++++++++---- priv/gettext/en/LC_MESSAGES/default.po | 120 +++++++++-------- 3 files changed, 233 insertions(+), 163 deletions(-) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 30d8ad6..45723e7 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -13,10 +13,10 @@ msgstr "" #: lib/mv_web/components/core_components.ex:339 #, elixir-autogen, elixir-format msgid "Actions" -msgstr "" +msgstr "Aktionen" #: lib/mv_web/live/member_live/index.html.heex:77 -#: lib/mv_web/live/user_live/index.ex:36 +#: lib/mv_web/live/user_live/index.html.heex:69 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "Bist du sicher?" @@ -35,14 +35,14 @@ msgid "City" msgstr "Stadt" #: lib/mv_web/live/member_live/index.html.heex:79 -#: lib/mv_web/live/user_live/index.ex:38 +#: lib/mv_web/live/user_live/index.html.heex:71 #, elixir-autogen, elixir-format msgid "Delete" msgstr "Löschen" #: lib/mv_web/live/member_live/index.html.heex:71 -#: lib/mv_web/live/user_live/form.ex:51 -#: lib/mv_web/live/user_live/index.ex:30 +#: lib/mv_web/live/user_live/form.ex:109 +#: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Edit" msgstr "Bearbeite" @@ -57,7 +57,7 @@ msgstr "Mitglied bearbeiten" #: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/user_live/form.ex:14 -#: lib/mv_web/live/user_live/index.ex:22 +#: lib/mv_web/live/user_live/index.html.heex:48 #: lib/mv_web/live/user_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Email" @@ -88,7 +88,7 @@ msgid "New Member" msgstr "Neues Mitglied" #: lib/mv_web/live/member_live/index.html.heex:68 -#: lib/mv_web/live/user_live/index.ex:27 +#: lib/mv_web/live/user_live/index.html.heex:60 #, elixir-autogen, elixir-format msgid "Show" msgstr "Anzeigen" @@ -167,7 +167,7 @@ msgstr "Mitglied speichern" #: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/property_type_live/form.ex:29 -#: lib/mv_web/live/user_live/form.ex:34 +#: lib/mv_web/live/user_live/form.ex:92 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "Speichern..." @@ -226,7 +226,7 @@ msgstr "aktualisiert" #: lib/mv_web/controllers/auth_controller.ex:43 #, elixir-autogen, elixir-format msgid "Incorrect email or password" -msgstr "" +msgstr "Falsche E-Mail oder Passwort" #: lib/mv_web/live/member_live/form.ex:115 #, elixir-autogen, elixir-format @@ -236,50 +236,50 @@ msgstr "Mitglied %{action} erfolgreich" #: lib/mv_web/controllers/auth_controller.ex:14 #, elixir-autogen, elixir-format msgid "You are now signed in" -msgstr "" +msgstr "Sie sind jetzt angemeldet" #: lib/mv_web/controllers/auth_controller.ex:56 #, elixir-autogen, elixir-format msgid "You are now signed out" -msgstr "" +msgstr "Sie sind jetzt abgemeldet" #: lib/mv_web/controllers/auth_controller.ex:37 #, elixir-autogen, elixir-format msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n" -msgstr "" +msgstr "Sie haben sich bereits auf andere Weise angemeldet, aber Ihr Konto noch nicht bestätigt.\nSie können Ihr Konto über den Link bestätigen, den wir Ihnen gesendet haben, oder durch Zurücksetzen Ihres Passworts.\n" #: lib/mv_web/controllers/auth_controller.ex:12 #, elixir-autogen, elixir-format msgid "Your email address has now been confirmed" -msgstr "" +msgstr "Ihre E-Mail-Adresse wurde bestätigt" #: lib/mv_web/controllers/auth_controller.ex:13 #, elixir-autogen, elixir-format msgid "Your password has successfully been reset" -msgstr "" +msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/property_type_live/form.ex:32 -#: lib/mv_web/live/user_live/form.ex:37 +#: lib/mv_web/live/user_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Cancel" -msgstr "" +msgstr "Abbrechen" #: lib/mv_web/live/property_live/form.ex:29 #, elixir-autogen, elixir-format msgid "Choose a member" -msgstr "" +msgstr "Mitglied auswählen" #: lib/mv_web/live/property_live/form.ex:20 #, elixir-autogen, elixir-format msgid "Choose a property type" -msgstr "" +msgstr "Eigenschaftstyp auswählen" #: lib/mv_web/live/property_type_live/form.ex:25 #, elixir-autogen, elixir-format msgid "Description" -msgstr "" +msgstr "Beschreibung" #: lib/mv_web/live/user_live/show.ex:17 #, elixir-autogen, elixir-format @@ -299,15 +299,15 @@ msgstr "ID" #: lib/mv_web/live/property_type_live/form.ex:26 #, elixir-autogen, elixir-format msgid "Immutable" -msgstr "" +msgstr "Unveränderlich" #: lib/mv_web/components/layouts/navbar.ex:73 #, elixir-autogen, elixir-format msgid "Logout" -msgstr "" +msgstr "Abmelden" -#: lib/mv_web/live/user_live/index.ex:9 -#: lib/mv_web/live/user_live/index.ex:50 +#: lib/mv_web/live/user_live/index.ex:12 +#: lib/mv_web/live/user_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Listing Users" msgstr "Benutzer auflisten" @@ -315,7 +315,7 @@ msgstr "Benutzer auflisten" #: lib/mv_web/live/property_live/form.ex:27 #, elixir-autogen, elixir-format msgid "Member" -msgstr "" +msgstr "Mitglied" #: lib/mv_web/components/layouts/navbar.ex:14 #: lib/mv_web/live/member_live/index.ex:12 @@ -328,9 +328,9 @@ msgstr "Mitglieder" #: lib/mv_web/live/property_type_live/form.ex:16 #, elixir-autogen, elixir-format msgid "Name" -msgstr "" +msgstr "Name" -#: lib/mv_web/live/user_live/index.ex:12 +#: lib/mv_web/live/user_live/index.html.heex:6 #, elixir-autogen, elixir-format msgid "New User" msgstr "Neuer Benutzer" @@ -345,13 +345,13 @@ msgstr "Nicht aktiviert" msgid "Not set" msgstr "Nicht gesetzt" -#: lib/mv_web/live/user_live/form.ex:19 -#: lib/mv_web/live/user_live/form.ex:27 +#: lib/mv_web/live/user_live/form.ex:75 +#: lib/mv_web/live/user_live/form.ex:83 #, elixir-autogen, elixir-format msgid "Note" msgstr "Hinweis" -#: lib/mv_web/live/user_live/index.ex:23 +#: lib/mv_web/live/user_live/index.html.heex:56 #: lib/mv_web/live/user_live/show.ex:25 #, elixir-autogen, elixir-format msgid "OIDC ID" @@ -362,20 +362,15 @@ msgstr "OIDC ID" msgid "Password Authentication" msgstr "Passwort-Authentifizierung" -#: lib/mv_web/live/user_live/form.ex:19 -#, elixir-autogen, elixir-format -msgid "Password can only be changed through authentication functions." -msgstr "" - #: lib/mv_web/live/property_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Please select a property type first" -msgstr "" +msgstr "Bitte wählen Sie zuerst einen Eigenschaftstyp" #: lib/mv_web/components/layouts/navbar.ex:69 #, elixir-autogen, elixir-format msgid "Profil" -msgstr "" +msgstr "Profil" #: lib/mv_web/live/property_live/form.ex:207 #, elixir-autogen, elixir-format, fuzzy @@ -385,27 +380,27 @@ msgstr "Mitglied %{action} erfolgreich" #: lib/mv_web/live/property_live/form.ex:18 #, elixir-autogen, elixir-format msgid "Property type" -msgstr "" +msgstr "Eigenschaftstyp" #: lib/mv_web/live/property_type_live/form.ex:80 #, elixir-autogen, elixir-format msgid "Property type %{action} successfully" -msgstr "" +msgstr "Eigenschaftstyp %{action} erfolgreich" #: lib/mv_web/live/property_type_live/form.ex:27 #, elixir-autogen, elixir-format msgid "Required" -msgstr "" +msgstr "Erforderlich" #: lib/mv_web/live/property_live/form.ex:42 #, elixir-autogen, elixir-format msgid "Save Property" -msgstr "" +msgstr "Eigenschaft speichern" #: lib/mv_web/live/property_type_live/form.ex:30 #, elixir-autogen, elixir-format msgid "Save Property type" -msgstr "" +msgstr "Eigenschaftstyp speichern" #: lib/mv_web/live/member_live/index.html.heex:27 #, elixir-autogen, elixir-format @@ -420,9 +415,9 @@ msgstr "Mitglied auswählen" #: lib/mv_web/components/layouts/navbar.ex:72 #, elixir-autogen, elixir-format msgid "Settings" -msgstr "" +msgstr "Einstellungen" -#: lib/mv_web/live/user_live/form.ex:35 +#: lib/mv_web/live/user_live/form.ex:93 #, elixir-autogen, elixir-format msgid "Save User" msgstr "Benutzer speichern" @@ -440,7 +435,7 @@ msgstr "Dies ist ein Benutzer-Datensatz aus Ihrer Datenbank." #: lib/mv_web/live/property_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Unsupported value type: %{type}" -msgstr "" +msgstr "Nicht unterstützter Wertetyp: %{type}" #: lib/mv_web/live/property_live/form.ex:10 #, elixir-autogen, elixir-format, fuzzy @@ -457,7 +452,7 @@ msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenscha msgid "Use this form to manage user records in your database." msgstr "Verwenden Sie dieses Formular, um Benutzer-Datensätze zu verwalten." -#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/form.ex:110 #: lib/mv_web/live/user_live/show.ex:9 #, elixir-autogen, elixir-format msgid "User" @@ -466,12 +461,12 @@ msgstr "Benutzer" #: lib/mv_web/live/property_live/form.ex:59 #, elixir-autogen, elixir-format msgid "Value" -msgstr "" +msgstr "Wert" #: lib/mv_web/live/property_type_live/form.ex:20 #, elixir-autogen, elixir-format msgid "Value type" -msgstr "" +msgstr "Wertetyp" #: lib/mv_web/components/table_components.ex:30 #, elixir-autogen, elixir-format @@ -483,72 +478,77 @@ msgstr "aufsteigend" msgid "descending" msgstr "absteigend" -#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/form.ex:109 #, elixir-autogen, elixir-format msgid "New" msgstr "Neuer" +#: lib/mv_web/live/user_live/form.ex:64 +#, elixir-autogen, elixir-format +msgid "Admin Note" +msgstr "Administrator-Hinweis" + +#: lib/mv_web/live/user_live/form.ex:64 +#, 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:55 +#, elixir-autogen, elixir-format +msgid "At least 8 characters" +msgstr "Mindestens 8 Zeichen" + #: lib/mv_web/live/user_live/form.ex:27 #, elixir-autogen, elixir-format -msgid "Users created here will need to set their password through the authentication system." -msgstr "Hier erstellte Benutzer müssen ihr Passwort über das Authentifizierungssystem setzen." +msgid "Change Password" +msgstr "Passwort ändern" -#: lib/mv_web/live/user_live/index.html.heex:15 +#: lib/mv_web/live/user_live/form.ex:75 #, elixir-autogen, elixir-format -msgid "Select all users" -msgstr "Alle Benutzer auswählen" +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." -#: lib/mv_web/live/user_live/index.html.heex:29 +#: lib/mv_web/live/user_live/form.ex:45 #, elixir-autogen, elixir-format -msgid "Select user" -msgstr "Benutzer auswählen" +msgid "Confirm Password" +msgstr "Passwort bestätigen" -#: lib/mv_web/live/user_live/form.ex:23 +#: lib/mv_web/live/user_live/form.ex:57 #, elixir-autogen, elixir-format -msgid "Set Password" -msgstr "Passwort setzen" +msgid "Consider using special characters" +msgstr "Sonderzeichen empfohlen" + +#: lib/mv_web/live/user_live/form.ex:56 +#, elixir-autogen, elixir-format +msgid "Include both letters and numbers" +msgstr "Buchstaben und Zahlen verwenden" #: 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 +#: lib/mv_web/live/user_live/form.ex:53 #, elixir-autogen, elixir-format msgid "Password requirements" msgstr "Passwort-Anforderungen" -#: lib/mv_web/live/user_live/form.ex:50 +#: lib/mv_web/live/user_live/index.html.heex:25 #, elixir-autogen, elixir-format -msgid "At least 8 characters" -msgstr "Mindestens 8 Zeichen" +msgid "Select all users" +msgstr "Alle Benutzer auswählen" -#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/index.html.heex:39 #, elixir-autogen, elixir-format -msgid "Include both letters and numbers" -msgstr "Buchstaben und Zahlen verwenden" +msgid "Select user" +msgstr "Benutzer auswählen" -#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/form.ex:27 #, elixir-autogen, elixir-format -msgid "Consider using special characters" -msgstr "Sonderzeichen empfohlen" +msgid "Set Password" +msgstr "Passwort setzen" -#: lib/mv_web/live/user_live/form.ex:57 +#: lib/mv_web/live/user_live/form.ex:83 #, 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." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index baa059a..fd064d2 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -17,7 +17,7 @@ msgid "Actions" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:77 -#: lib/mv_web/live/user_live/index.ex:36 +#: lib/mv_web/live/user_live/index.html.heex:69 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" @@ -36,14 +36,14 @@ msgid "City" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:79 -#: lib/mv_web/live/user_live/index.ex:38 +#: lib/mv_web/live/user_live/index.html.heex:71 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:71 -#: lib/mv_web/live/user_live/form.ex:51 -#: lib/mv_web/live/user_live/index.ex:30 +#: lib/mv_web/live/user_live/form.ex:109 +#: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Edit" msgstr "" @@ -58,7 +58,7 @@ msgstr "" #: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/user_live/form.ex:14 -#: lib/mv_web/live/user_live/index.ex:22 +#: lib/mv_web/live/user_live/index.html.heex:48 #: lib/mv_web/live/user_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Email" @@ -89,7 +89,7 @@ msgid "New Member" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:68 -#: lib/mv_web/live/user_live/index.ex:27 +#: lib/mv_web/live/user_live/index.html.heex:60 #, elixir-autogen, elixir-format msgid "Show" msgstr "" @@ -168,7 +168,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/property_type_live/form.ex:29 -#: lib/mv_web/live/user_live/form.ex:34 +#: lib/mv_web/live/user_live/form.ex:92 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "" @@ -262,7 +262,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/property_type_live/form.ex:32 -#: lib/mv_web/live/user_live/form.ex:37 +#: lib/mv_web/live/user_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" @@ -307,8 +307,8 @@ msgstr "" msgid "Logout" msgstr "" -#: lib/mv_web/live/user_live/index.ex:9 -#: lib/mv_web/live/user_live/index.ex:50 +#: lib/mv_web/live/user_live/index.ex:12 +#: lib/mv_web/live/user_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Listing Users" msgstr "" @@ -331,7 +331,7 @@ msgstr "" msgid "Name" msgstr "" -#: lib/mv_web/live/user_live/index.ex:12 +#: lib/mv_web/live/user_live/index.html.heex:6 #, elixir-autogen, elixir-format msgid "New User" msgstr "" @@ -346,13 +346,13 @@ msgstr "" msgid "Not set" msgstr "" -#: lib/mv_web/live/user_live/form.ex:19 -#: lib/mv_web/live/user_live/form.ex:27 +#: lib/mv_web/live/user_live/form.ex:75 +#: lib/mv_web/live/user_live/form.ex:83 #, elixir-autogen, elixir-format msgid "Note" msgstr "" -#: lib/mv_web/live/user_live/index.ex:23 +#: lib/mv_web/live/user_live/index.html.heex:56 #: lib/mv_web/live/user_live/show.ex:25 #, elixir-autogen, elixir-format msgid "OIDC ID" @@ -363,11 +363,6 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/live/user_live/form.ex:19 -#, elixir-autogen, elixir-format -msgid "Password can only be changed through authentication functions." -msgstr "" - #: lib/mv_web/live/property_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Please select a property type first" @@ -423,7 +418,7 @@ msgstr "" msgid "Settings" msgstr "" -#: lib/mv_web/live/user_live/form.ex:35 +#: lib/mv_web/live/user_live/form.ex:93 #, elixir-autogen, elixir-format msgid "Save User" msgstr "" @@ -458,7 +453,7 @@ msgstr "" msgid "Use this form to manage user records in your database." msgstr "" -#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/form.ex:110 #: lib/mv_web/live/user_live/show.ex:9 #, elixir-autogen, elixir-format msgid "User" @@ -484,12 +479,77 @@ msgstr "" msgid "descending" msgstr "" -#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/form.ex:109 #, elixir-autogen, elixir-format msgid "New" msgstr "" +#: lib/mv_web/live/user_live/form.ex:64 +#, elixir-autogen, elixir-format +msgid "Admin Note" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:64 +#, 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 "" + +#: lib/mv_web/live/user_live/form.ex:55 +#, elixir-autogen, elixir-format +msgid "At least 8 characters" +msgstr "" + #: lib/mv_web/live/user_live/form.ex:27 #, elixir-autogen, elixir-format -msgid "Users created here will need to set their password through the authentication system." +msgid "Change Password" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:75 +#, elixir-autogen, elixir-format +msgid "Check 'Change Password' above to set a new password for this user." +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:45 +#, elixir-autogen, elixir-format +msgid "Confirm Password" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:57 +#, elixir-autogen, elixir-format +msgid "Consider using special characters" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:56 +#, elixir-autogen, elixir-format +msgid "Include both letters and numbers" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:35 +#, elixir-autogen, elixir-format +msgid "Password" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:53 +#, elixir-autogen, elixir-format +msgid "Password requirements" +msgstr "" + +#: lib/mv_web/live/user_live/index.html.heex:25 +#, elixir-autogen, elixir-format +msgid "Select all users" +msgstr "" + +#: lib/mv_web/live/user_live/index.html.heex:39 +#, elixir-autogen, elixir-format +msgid "Select user" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:27 +#, elixir-autogen, elixir-format +msgid "Set Password" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:83 +#, elixir-autogen, elixir-format +msgid "User will be created without a password. Check 'Set Password' to add one." msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 0536761..41a35b4 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -17,7 +17,7 @@ msgid "Actions" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:77 -#: lib/mv_web/live/user_live/index.ex:36 +#: lib/mv_web/live/user_live/index.html.heex:69 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" @@ -36,14 +36,14 @@ msgid "City" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:79 -#: lib/mv_web/live/user_live/index.ex:38 +#: lib/mv_web/live/user_live/index.html.heex:71 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:71 -#: lib/mv_web/live/user_live/form.ex:51 -#: lib/mv_web/live/user_live/index.ex:30 +#: lib/mv_web/live/user_live/form.ex:109 +#: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Edit" msgstr "" @@ -58,7 +58,7 @@ msgstr "" #: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/user_live/form.ex:14 -#: lib/mv_web/live/user_live/index.ex:22 +#: lib/mv_web/live/user_live/index.html.heex:48 #: lib/mv_web/live/user_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Email" @@ -89,7 +89,7 @@ msgid "New Member" msgstr "" #: lib/mv_web/live/member_live/index.html.heex:68 -#: lib/mv_web/live/user_live/index.ex:27 +#: lib/mv_web/live/user_live/index.html.heex:60 #, elixir-autogen, elixir-format msgid "Show" msgstr "" @@ -168,7 +168,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/property_type_live/form.ex:29 -#: lib/mv_web/live/user_live/form.ex:34 +#: lib/mv_web/live/user_live/form.ex:92 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "" @@ -262,7 +262,7 @@ msgstr "" #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/property_type_live/form.ex:32 -#: lib/mv_web/live/user_live/form.ex:37 +#: lib/mv_web/live/user_live/form.ex:95 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" @@ -307,8 +307,8 @@ msgstr "" msgid "Logout" msgstr "" -#: lib/mv_web/live/user_live/index.ex:9 -#: lib/mv_web/live/user_live/index.ex:50 +#: lib/mv_web/live/user_live/index.ex:12 +#: lib/mv_web/live/user_live/index.html.heex:3 #, elixir-autogen, elixir-format, fuzzy msgid "Listing Users" msgstr "" @@ -331,7 +331,7 @@ msgstr "" msgid "Name" msgstr "" -#: lib/mv_web/live/user_live/index.ex:12 +#: lib/mv_web/live/user_live/index.html.heex:6 #, elixir-autogen, elixir-format msgid "New User" msgstr "" @@ -346,13 +346,13 @@ msgstr "" msgid "Not set" msgstr "" -#: lib/mv_web/live/user_live/form.ex:19 -#: lib/mv_web/live/user_live/form.ex:27 +#: lib/mv_web/live/user_live/form.ex:75 +#: lib/mv_web/live/user_live/form.ex:83 #, elixir-autogen, elixir-format, fuzzy msgid "Note" msgstr "" -#: lib/mv_web/live/user_live/index.ex:23 +#: lib/mv_web/live/user_live/index.html.heex:56 #: lib/mv_web/live/user_live/show.ex:25 #, elixir-autogen, elixir-format msgid "OIDC ID" @@ -363,11 +363,6 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/live/user_live/form.ex:19 -#, elixir-autogen, elixir-format -msgid "Password can only be changed through authentication functions." -msgstr "" - #: lib/mv_web/live/property_live/form.ex:37 #, elixir-autogen, elixir-format msgid "Please select a property type first" @@ -423,7 +418,7 @@ msgstr "" msgid "Settings" msgstr "" -#: lib/mv_web/live/user_live/form.ex:35 +#: lib/mv_web/live/user_live/form.ex:93 #, elixir-autogen, elixir-format, fuzzy msgid "Save User" msgstr "" @@ -458,7 +453,7 @@ msgstr "" msgid "Use this form to manage user records in your database." msgstr "" -#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/form.ex:110 #: lib/mv_web/live/user_live/show.ex:9 #, elixir-autogen, elixir-format msgid "User" @@ -484,62 +479,77 @@ msgstr "" msgid "descending" msgstr "" -#: lib/mv_web/live/user_live/form.ex:51 +#: lib/mv_web/live/user_live/form.ex:109 #, elixir-autogen, elixir-format msgid "New" msgstr "" +#: lib/mv_web/live/user_live/form.ex:64 +#, elixir-autogen, elixir-format +msgid "Admin Note" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex:64 +#, 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:55 +#, elixir-autogen, elixir-format +msgid "At least 8 characters" +msgstr "At least 8 characters" + #: lib/mv_web/live/user_live/form.ex:27 #, elixir-autogen, elixir-format -msgid "Users created here will need to set their password through the authentication system." -msgstr "Users created here will need to set their password through the authentication system." +msgid "Change Password" +msgstr "" -#: lib/mv_web/live/user_live/form.ex:23 +#: lib/mv_web/live/user_live/form.ex:75 #, elixir-autogen, elixir-format -msgid "Set Password" -msgstr "Set Password" +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." + +#: lib/mv_web/live/user_live/form.ex:45 +#, elixir-autogen, elixir-format +msgid "Confirm Password" +msgstr "Confirm Password" + +#: lib/mv_web/live/user_live/form.ex:57 +#, elixir-autogen, elixir-format +msgid "Consider using special characters" +msgstr "Consider using special characters" + +#: lib/mv_web/live/user_live/form.ex:56 +#, elixir-autogen, elixir-format +msgid "Include both letters and numbers" +msgstr "Include both letters and numbers" #: 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 +#: lib/mv_web/live/user_live/form.ex:53 #, 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/index.html.heex:25 +#, elixir-autogen, elixir-format, fuzzy +msgid "Select all users" +msgstr "" -#: 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/index.html.heex:39 +#, elixir-autogen, elixir-format, fuzzy +msgid "Select user" +msgstr "" -#: lib/mv_web/live/user_live/form.ex:52 +#: lib/mv_web/live/user_live/form.ex:27 #, elixir-autogen, elixir-format -msgid "Consider using special characters" -msgstr "Consider using special characters" +msgid "Set Password" +msgstr "Set Password" -#: lib/mv_web/live/user_live/form.ex:57 +#: lib/mv_web/live/user_live/form.ex:83 #, 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." -- 2.47.2 From 7a75f8f5267ffe03bdd5b54922993f0d83388906 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 22 Jul 2025 23:05:24 +0200 Subject: [PATCH 08/10] feat: add user form tests --- test/mv_web/user_live/form_test.exs | 263 ++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 test/mv_web/user_live/form_test.exs diff --git a/test/mv_web/user_live/form_test.exs b/test/mv_web/user_live/form_test.exs new file mode 100644 index 0000000..7988f3e --- /dev/null +++ b/test/mv_web/user_live/form_test.exs @@ -0,0 +1,263 @@ +defmodule MvWeb.UserLive.FormTest do + use MvWeb.ConnCase, async: true + import Phoenix.LiveViewTest + + # Helper to setup authenticated connection and live view + defp setup_live_view(conn, path) do + conn = conn_with_oidc_user(conn, %{email: "admin@example.com"}) + live(conn, path) + end + + describe "new user form - display" do + test "shows correct form elements", %{conn: conn} do + {:ok, view, html} = setup_live_view(conn, "/users/new") + + assert html =~ "New User" + assert html =~ "Email" + assert html =~ "Set Password" + assert has_element?(view, "form#user-form[phx-submit='save']") + assert has_element?(view, "input[name='user[email]']") + assert has_element?(view, "input[type='checkbox'][name='set_password']") + end + + test "hides password fields initially", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + refute has_element?(view, "input[name='user[password]']") + refute has_element?(view, "input[name='user[password_confirmation]']") + end + + test "shows password fields when checkbox toggled", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + view |> element("input[name='set_password']") |> render_click() + + assert has_element?(view, "input[name='user[password]']") + assert has_element?(view, "input[name='user[password_confirmation]']") + assert render(view) =~ "Password requirements" + end + end + + describe "new user form - creation" do + test "creates user without password", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + view + |> form("#user-form", user: %{email: "newuser@example.com"}) + |> render_submit() + + assert_redirected(view, "/users") + end + + test "creates user with password when enabled", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + view |> element("input[name='set_password']") |> render_click() + + view + |> form("#user-form", user: %{ + email: "passworduser@example.com", + password: "securepassword123", + password_confirmation: "securepassword123" + }) + |> render_submit() + + assert_redirected(view, "/users") + end + + test "stores user data correctly", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + view + |> form("#user-form", user: %{email: "storetest@example.com"}) + |> render_submit() + + user = Ash.get!(Mv.Accounts.User, + [email: Ash.CiString.new("storetest@example.com")], + domain: Mv.Accounts + ) + assert to_string(user.email) == "storetest@example.com" + assert is_nil(user.hashed_password) + end + + test "stores password when provided", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + view |> element("input[name='set_password']") |> render_click() + + view + |> form("#user-form", user: %{ + email: "passwordstoretest@example.com", + password: "securepassword123", + password_confirmation: "securepassword123" + }) + |> render_submit() + + user = Ash.get!(Mv.Accounts.User, + [email: Ash.CiString.new("passwordstoretest@example.com")], + domain: Mv.Accounts + ) + assert user.hashed_password != nil + assert String.starts_with?(user.hashed_password, "$2b$") + end + end + + describe "new user form - validation" do + test "shows error for duplicate email", %{conn: conn} do + _existing_user = create_test_user(%{email: "existing@example.com"}) + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + html = view + |> form("#user-form", user: %{email: "existing@example.com"}) + |> render_submit() + + assert html =~ "has already been taken" + end + + test "shows error for short password", %{conn: conn} do + {:ok, view, _html} = setup_live_view(conn, "/users/new") + + view |> element("input[name='set_password']") |> render_click() + + html = view + |> form("#user-form", user: %{ + email: "test@example.com", + password: "123", + password_confirmation: "123" + }) + |> render_submit() + + assert html =~ "length must be greater than or equal to 8" + end + end + + describe "edit user form - display" do + test "shows correct form elements for existing user", %{conn: conn} do + user = create_test_user(%{email: "editme@example.com"}) + {:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit") + + assert html =~ "Edit User" + assert html =~ "Change Password" + assert has_element?(view, "input[name='user[email]'][value='editme@example.com']") + assert html =~ "Check 'Change Password' above to set a new password for this user" + end + + test "shows admin password fields when enabled", %{conn: conn} do + user = create_test_user(%{email: "editme@example.com"}) + {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") + + view |> element("input[name='set_password']") |> render_click() + + assert has_element?(view, "input[name='user[password]']") + refute has_element?(view, "input[name='user[password_confirmation]']") + assert render(view) =~ "Admin Note" + end + end + + describe "edit user form - updates" do + test "updates email without changing password", %{conn: conn} do + user = create_test_user(%{email: "old@example.com"}) + original_password = user.hashed_password + {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") + + view + |> form("#user-form", user: %{email: "new@example.com"}) + |> render_submit() + + assert_redirected(view, "/users") + + updated_user = Ash.reload!(user, domain: Mv.Accounts) + assert to_string(updated_user.email) == "new@example.com" + assert updated_user.hashed_password == original_password + end + + test "admin sets new password for user", %{conn: conn} do + user = create_test_user(%{email: "user@example.com"}) + original_password = user.hashed_password + {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") + + view |> element("input[name='set_password']") |> render_click() + + view + |> form("#user-form", user: %{ + email: "user@example.com", + password: "newadminpassword123" + }) + |> render_submit() + + assert_redirected(view, "/users") + + updated_user = Ash.reload!(user, domain: Mv.Accounts) + assert updated_user.hashed_password != original_password + assert String.starts_with?(updated_user.hashed_password, "$2b$") + end + end + + describe "edit user form - validation" do + test "shows error for duplicate email", %{conn: conn} do + _existing_user = create_test_user(%{email: "taken@example.com"}) + user_to_edit = create_test_user(%{email: "original@example.com"}) + {:ok, view, _html} = setup_live_view(conn, "/users/#{user_to_edit.id}/edit") + + html = view + |> form("#user-form", user: %{email: "taken@example.com"}) + |> render_submit() + + assert html =~ "has already been taken" + end + + test "shows error for invalid password", %{conn: conn} do + user = create_test_user(%{email: "user@example.com"}) + {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") + + view |> element("input[name='set_password']") |> render_click() + + result = view + |> form("#user-form", user: %{ + email: "user@example.com", + password: "123" + }) + |> render_submit() + + case result do + {:error, {:live_redirect, %{to: "/users"}}} -> + flunk("Expected validation error but form was submitted successfully") + html when is_binary(html) -> + assert html =~ "must have length of at least 8" + end + end + end + + describe "internationalization" do + test "shows German labels", %{conn: conn} do + conn = conn_with_oidc_user(conn, %{email: "admin_de@example.com"}) + conn = Plug.Test.init_test_session(conn, locale: "de") + {:ok, _view, html} = live(conn, "/users/new") + + assert html =~ "Neuer Benutzer" + assert html =~ "E-Mail" + assert html =~ "Passwort setzen" + end + + test "shows English labels", %{conn: conn} do + conn = conn_with_oidc_user(conn, %{email: "admin_en@example.com"}) + Gettext.put_locale(MvWeb.Gettext, "en") + {:ok, _view, html} = live(conn, "/users/new") + + assert html =~ "New User" + assert html =~ "Email" + assert html =~ "Set Password" + end + + test "shows different labels for edit vs new", %{conn: conn} do + user = create_test_user(%{email: "test@example.com"}) + conn = conn_with_oidc_user(conn, %{email: "admin@example.com"}) + + {:ok, _view, new_html} = live(conn, "/users/new") + {:ok, _view, edit_html} = live(conn, "/users/#{user.id}/edit") + + assert new_html =~ "Set Password" + assert edit_html =~ "Change Password" + end + end +end \ No newline at end of file -- 2.47.2 From 33d4fa66c8c2eaf940e29918d822042a2515f11a Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 22 Jul 2025 23:59:01 +0200 Subject: [PATCH 09/10] fix: update email field given by oidc provider --- lib/accounts/user.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index daf29ba..970e65f 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -96,14 +96,14 @@ defmodule Mv.Accounts.User do argument :oauth_tokens, :map, allow_nil?: false prepare AshAuthentication.Strategy.OAuth2.SignInPreparation - filter expr(email == get_path(^arg(:user_info), [:email])) + filter expr(email == get_path(^arg(:user_info), [:preferred_username])) end create :register_with_rauthy do argument :user_info, :map, allow_nil?: false argument :oauth_tokens, :map, allow_nil?: false upsert? true - upsert_identity :unique_email + upsert_identity :unique_oidc_id change AshAuthentication.GenerateTokenChange -- 2.47.2 From 06574a932de49f35f2e7ac77f64338fd2b54cd47 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 23 Jul 2025 00:04:03 +0200 Subject: [PATCH 10/10] fix: formatting --- lib/accounts/user.ex | 20 ++-- lib/mv_web/live/user_live/form.ex | 37 ++++--- lib/mv_web/live/user_live/index.html.heex | 8 +- test/mv_web/user_live/form_test.exs | 121 +++++++++++++--------- test/mv_web/user_live/index_test.exs | 107 ++++++++++++------- test/support/conn_case.ex | 53 +++++----- 6 files changed, 201 insertions(+), 145 deletions(-) diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 970e65f..2da15a1 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -76,10 +76,10 @@ defmodule Mv.Accounts.User do 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 @@ -117,6 +117,14 @@ defmodule Mv.Accounts.User do 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 + attributes do uuid_primary_key :id @@ -134,14 +142,6 @@ defmodule Mv.Accounts.User do identity :unique_oidc_id, [:oidc_id] 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 # only allows user data to be interacted with via AshAuthentication. # policies do diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index b599bc1..e77283a 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -12,8 +12,8 @@ defmodule MvWeb.UserLive.Form do <.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.input field={@form[:email]} label={gettext("Email")} required type="email" /> - - + +
- + <%= if @show_password_fields do %>
- <.input - field={@form[:password]} - label={gettext("Password")} - type="password" + <.input + field={@form[:password]} + label={gettext("Password")} + type="password" required autocomplete="new-password" /> - + <%= if !@user do %> - <.input - field={@form[:password_confirmation]} - label={gettext("Confirm Password")} - type="password" + <.input + field={@form[:password_confirmation]} + label={gettext("Confirm Password")} + type="password" required autocomplete="new-password" /> <% end %> - +

{gettext("Password requirements")}:

    @@ -57,7 +57,7 @@ defmodule MvWeb.UserLive.Form do
  • {gettext("Consider using special characters")}
- + <%= if @user do %>

@@ -124,17 +124,15 @@ defmodule MvWeb.UserLive.Form do @impl true def handle_event("toggle_password_section", _params, socket) do show_password_fields = !socket.assigns.show_password_fields - - socket = + + socket = socket |> assign(:show_password_fields, show_password_fields) |> assign_form() - + {:noreply, socket} end - - def handle_event("validate", %{"user" => user_params}, socket) do {:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))} end @@ -167,6 +165,7 @@ defmodule MvWeb.UserLive.Form do else # 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, as: "user" diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex index 5b313f0..258779e 100644 --- a/lib/mv_web/live/user_live/index.html.heex +++ b/lib/mv_web/live/user_live/index.html.heex @@ -8,11 +8,7 @@ - <.table - id="users" - rows={@users} - row_click={fn user -> JS.navigate(~p"/users/#{user}") end} - > + <.table id="users" rows={@users} row_click={fn user -> JS.navigate(~p"/users/#{user}") end}> <:col :let={user} label={ @@ -72,4 +68,4 @@ - \ No newline at end of file + diff --git a/test/mv_web/user_live/form_test.exs b/test/mv_web/user_live/form_test.exs index 7988f3e..decc789 100644 --- a/test/mv_web/user_live/form_test.exs +++ b/test/mv_web/user_live/form_test.exs @@ -13,7 +13,7 @@ defmodule MvWeb.UserLive.FormTest do {:ok, view, html} = setup_live_view(conn, "/users/new") assert html =~ "New User" - assert html =~ "Email" + assert html =~ "Email" assert html =~ "Set Password" assert has_element?(view, "form#user-form[phx-submit='save']") assert has_element?(view, "input[name='user[email]']") @@ -53,13 +53,15 @@ defmodule MvWeb.UserLive.FormTest do {:ok, view, _html} = setup_live_view(conn, "/users/new") view |> element("input[name='set_password']") |> render_click() - + view - |> form("#user-form", user: %{ - email: "passworduser@example.com", - password: "securepassword123", - password_confirmation: "securepassword123" - }) + |> form("#user-form", + user: %{ + email: "passworduser@example.com", + password: "securepassword123", + password_confirmation: "securepassword123" + } + ) |> render_submit() assert_redirected(view, "/users") @@ -72,10 +74,13 @@ defmodule MvWeb.UserLive.FormTest do |> form("#user-form", user: %{email: "storetest@example.com"}) |> render_submit() - user = Ash.get!(Mv.Accounts.User, - [email: Ash.CiString.new("storetest@example.com")], - domain: Mv.Accounts - ) + user = + Ash.get!( + Mv.Accounts.User, + [email: Ash.CiString.new("storetest@example.com")], + domain: Mv.Accounts + ) + assert to_string(user.email) == "storetest@example.com" assert is_nil(user.hashed_password) end @@ -84,19 +89,24 @@ defmodule MvWeb.UserLive.FormTest do {:ok, view, _html} = setup_live_view(conn, "/users/new") view |> element("input[name='set_password']") |> render_click() - + view - |> form("#user-form", user: %{ - email: "passwordstoretest@example.com", - password: "securepassword123", - password_confirmation: "securepassword123" - }) + |> form("#user-form", + user: %{ + email: "passwordstoretest@example.com", + password: "securepassword123", + password_confirmation: "securepassword123" + } + ) |> render_submit() - user = Ash.get!(Mv.Accounts.User, - [email: Ash.CiString.new("passwordstoretest@example.com")], - domain: Mv.Accounts - ) + user = + Ash.get!( + Mv.Accounts.User, + [email: Ash.CiString.new("passwordstoretest@example.com")], + domain: Mv.Accounts + ) + assert user.hashed_password != nil assert String.starts_with?(user.hashed_password, "$2b$") end @@ -107,9 +117,10 @@ defmodule MvWeb.UserLive.FormTest do _existing_user = create_test_user(%{email: "existing@example.com"}) {:ok, view, _html} = setup_live_view(conn, "/users/new") - html = view - |> form("#user-form", user: %{email: "existing@example.com"}) - |> render_submit() + html = + view + |> form("#user-form", user: %{email: "existing@example.com"}) + |> render_submit() assert html =~ "has already been taken" end @@ -118,14 +129,17 @@ defmodule MvWeb.UserLive.FormTest do {:ok, view, _html} = setup_live_view(conn, "/users/new") view |> element("input[name='set_password']") |> render_click() - - html = view - |> form("#user-form", user: %{ - email: "test@example.com", - password: "123", - password_confirmation: "123" - }) - |> render_submit() + + html = + view + |> form("#user-form", + user: %{ + email: "test@example.com", + password: "123", + password_confirmation: "123" + } + ) + |> render_submit() assert html =~ "length must be greater than or equal to 8" end @@ -165,7 +179,7 @@ defmodule MvWeb.UserLive.FormTest do |> render_submit() assert_redirected(view, "/users") - + updated_user = Ash.reload!(user, domain: Mv.Accounts) assert to_string(updated_user.email) == "new@example.com" assert updated_user.hashed_password == original_password @@ -177,16 +191,18 @@ defmodule MvWeb.UserLive.FormTest do {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") view |> element("input[name='set_password']") |> render_click() - + view - |> form("#user-form", user: %{ - email: "user@example.com", - password: "newadminpassword123" - }) + |> form("#user-form", + user: %{ + email: "user@example.com", + password: "newadminpassword123" + } + ) |> render_submit() assert_redirected(view, "/users") - + updated_user = Ash.reload!(user, domain: Mv.Accounts) assert updated_user.hashed_password != original_password assert String.starts_with?(updated_user.hashed_password, "$2b$") @@ -199,9 +215,10 @@ defmodule MvWeb.UserLive.FormTest do user_to_edit = create_test_user(%{email: "original@example.com"}) {:ok, view, _html} = setup_live_view(conn, "/users/#{user_to_edit.id}/edit") - html = view - |> form("#user-form", user: %{email: "taken@example.com"}) - |> render_submit() + html = + view + |> form("#user-form", user: %{email: "taken@example.com"}) + |> render_submit() assert html =~ "has already been taken" end @@ -211,17 +228,21 @@ defmodule MvWeb.UserLive.FormTest do {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") view |> element("input[name='set_password']") |> render_click() - - result = view - |> form("#user-form", user: %{ - email: "user@example.com", - password: "123" - }) - |> render_submit() + + result = + view + |> form("#user-form", + user: %{ + email: "user@example.com", + password: "123" + } + ) + |> render_submit() case result do {:error, {:live_redirect, %{to: "/users"}}} -> flunk("Expected validation error but form was submitted successfully") + html when is_binary(html) -> assert html =~ "must have length of at least 8" end @@ -260,4 +281,4 @@ defmodule MvWeb.UserLive.FormTest do assert edit_html =~ "Change Password" end end -end \ No newline at end of file +end diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index ac4e65a..40756eb 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -65,12 +65,12 @@ defmodule MvWeb.UserLive.IndexTest do # Should show ascending indicator (up arrow) assert html =~ "hero-chevron-up" assert html =~ ~s(aria-sort="ascending") - + # Test actual sort order: alpha should appear before mike, mike before zulu alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0) mike_pos = html |> :binary.match("mike@example.com") |> elem(0) zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0) - + assert alpha_pos < mike_pos, "alpha@example.com should appear before mike@example.com" assert mike_pos < zulu_pos, "mike@example.com should appear before zulu@example.com" end @@ -78,21 +78,24 @@ defmodule MvWeb.UserLive.IndexTest do test "can sort email descending by clicking sort button", %{conn: conn} do conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, "/users") - + # Click on email sort button and get rendered result html = view |> element("button[phx-value-field='email']") |> render_click() # Should now show descending indicator (down arrow) assert html =~ "hero-chevron-down" assert html =~ ~s(aria-sort="descending") - + # Test actual sort order reversed: zulu should now appear before mike, mike before alpha alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0) mike_pos = html |> :binary.match("mike@example.com") |> elem(0) zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0) - - assert zulu_pos < mike_pos, "zulu@example.com should appear before mike@example.com when sorted desc" - assert mike_pos < alpha_pos, "mike@example.com should appear before alpha@example.com when sorted desc" + + assert zulu_pos < mike_pos, + "zulu@example.com should appear before mike@example.com when sorted desc" + + assert mike_pos < alpha_pos, + "mike@example.com should appear before alpha@example.com when sorted desc" end test "toggles back to ascending when clicking sort button twice", %{conn: conn} do @@ -106,12 +109,12 @@ defmodule MvWeb.UserLive.IndexTest do # Should be back to ascending assert html =~ "hero-chevron-up" assert html =~ ~s(aria-sort="ascending") - + # Should be back to original ascending order alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0) mike_pos = html |> :binary.match("mike@example.com") |> elem(0) zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0) - + assert alpha_pos < mike_pos, "Should be back to ascending: alpha before mike" assert mike_pos < zulu_pos, "Should be back to ascending: mike before zulu" end @@ -162,16 +165,20 @@ defmodule MvWeb.UserLive.IndexTest do # Initially, individual checkboxes should exist but not be checked assert view |> element("input[type='checkbox'][name='#{user1.id}']") |> has_element?() assert view |> element("input[type='checkbox'][name='#{user2.id}']") |> has_element?() - + # Initially, select_all should not be checked (since no individual items are selected) - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() # Select first user checkbox html = view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() # The select_all checkbox should still not be checked (not all users selected) - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() - + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() + # Page should still function normally assert html =~ "Email" assert html =~ to_string(user1.email) @@ -186,10 +193,12 @@ defmodule MvWeb.UserLive.IndexTest do # Then deselect user html = view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() - + # Select all should not be checked after deselecting individual user - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() - + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() + # Page should still function normally assert html =~ "Email" assert html =~ to_string(user1.email) @@ -200,16 +209,26 @@ defmodule MvWeb.UserLive.IndexTest do {:ok, view, _html} = live(conn, "/users") # Initially no checkboxes should be checked - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() - refute view |> element("input[type='checkbox'][name='#{user1.id}'][checked]") |> has_element?() - refute view |> element("input[type='checkbox'][name='#{user2.id}'][checked]") |> has_element?() + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() + + refute view + |> element("input[type='checkbox'][name='#{user1.id}'][checked]") + |> has_element?() + + refute view + |> element("input[type='checkbox'][name='#{user2.id}'][checked]") + |> has_element?() # Click select all html = view |> element("input[type='checkbox'][name='select_all']") |> render_click() # After selecting all, the select_all checkbox should be checked - assert view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() - + assert view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() + # Page should still function normally and show all users assert html =~ "Email" assert html =~ to_string(user1.email) @@ -222,35 +241,52 @@ defmodule MvWeb.UserLive.IndexTest do # Select all first view |> element("input[type='checkbox'][name='select_all']") |> render_click() - + # Verify that select_all is checked - assert view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + assert view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() # Then deselect all html = view |> element("input[type='checkbox'][name='select_all']") |> render_click() - + # After deselecting all, no checkboxes should be checked - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() - refute view |> element("input[type='checkbox'][name='#{user1.id}'][checked]") |> has_element?() - refute view |> element("input[type='checkbox'][name='#{user2.id}'][checked]") |> has_element?() - + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() + + refute view + |> element("input[type='checkbox'][name='#{user1.id}'][checked]") + |> has_element?() + + refute view + |> element("input[type='checkbox'][name='#{user2.id}'][checked]") + |> has_element?() + # Page should still function normally assert html =~ "Email" assert html =~ to_string(user1.email) assert html =~ to_string(user2.email) end - test "select all automatically checks when all individual users are selected", %{conn: conn, users: [user1, user2]} do + test "select all automatically checks when all individual users are selected", %{ + conn: conn, + users: [user1, user2] + } do conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, "/users") # Initially nothing should be checked - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() # Select first user view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click() # Select all should still not be checked (only 1 of 2+ users selected) - refute view |> element("input[type='checkbox'][name='select_all'][checked]") |> has_element?() + refute view + |> element("input[type='checkbox'][name='select_all'][checked]") + |> has_element?() # Select second user html = view |> element("input[type='checkbox'][name='#{user2.id}']") |> render_click() @@ -278,7 +314,8 @@ defmodule MvWeb.UserLive.IndexTest do # The page should still render (basic functionality test) html = render(view) - assert html =~ "Email" # Table header should still be there + # Table header should still be there + assert html =~ "Email" end test "shows delete confirmation", %{conn: conn} do @@ -336,7 +373,8 @@ defmodule MvWeb.UserLive.IndexTest do # Note: English translations might be empty strings by default # This test would verify the structure is there - assert html =~ ~s(aria-label=) # Checking that aria-label attributes exist + # Checking that aria-label attributes exist + assert html =~ ~s(aria-label=) end end @@ -371,5 +409,4 @@ defmodule MvWeb.UserLive.IndexTest do assert html =~ long_email end end - -end \ No newline at end of file +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 385083d..c51fb61 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -34,14 +34,14 @@ defmodule MvWeb.ConnCase do @doc """ Creates a test user and returns the user struct. Accepts attrs to override default values. - + Password handling: - If `hashed_password` is provided in attrs, it's used directly - If `password` is provided in attrs, it gets hashed automatically - If neither is provided, uses default password "password" - + ## Examples - + create_test_user() # Default user with unique email create_test_user(%{email: "custom@example.com"}) # Custom email create_test_user(%{password: "secret123"}) # Custom password (gets hashed) @@ -50,35 +50,38 @@ defmodule MvWeb.ConnCase do def create_test_user(attrs \\ %{}) do # Generate unique values to avoid conflicts unique_id = System.unique_integer([:positive]) - + default_attrs = %{ email: "user#{unique_id}@example.com", oidc_id: "oidc#{unique_id}" } - + # Merge provided attrs with defaults user_attrs = Map.merge(default_attrs, attrs) - + # Handle password/hashed_password - final_attrs = cond do - # If hashed_password is already provided, use it as-is - Map.has_key?(user_attrs, :hashed_password) -> - user_attrs - - # If password is provided, hash it - Map.has_key?(user_attrs, :password) -> - password = Map.get(user_attrs, :password) - {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) - user_attrs - |> Map.delete(:password) # Remove plain password - |> Map.put(:hashed_password, hashed_password) - - # Neither provided, use default password - true -> - password = "password" - {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) - Map.put(user_attrs, :hashed_password, hashed_password) - end + final_attrs = + cond do + # If hashed_password is already provided, use it as-is + Map.has_key?(user_attrs, :hashed_password) -> + user_attrs + + # If password is provided, hash it + Map.has_key?(user_attrs, :password) -> + password = Map.get(user_attrs, :password) + {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + + user_attrs + # Remove plain password + |> Map.delete(:password) + |> Map.put(:hashed_password, hashed_password) + + # Neither provided, use default password + true -> + password = "password" + {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + Map.put(user_attrs, :hashed_password, hashed_password) + end Ash.Seed.seed!(Mv.Accounts.User, final_attrs) end -- 2.47.2