diff --git a/lib/mv_web/helpers/user_helpers.ex b/lib/mv_web/helpers/user_helpers.ex new file mode 100644 index 0000000..2f9741c --- /dev/null +++ b/lib/mv_web/helpers/user_helpers.ex @@ -0,0 +1,58 @@ +defmodule MvWeb.Helpers.UserHelpers do + @moduledoc """ + Helper functions for user-related display in the web layer. + + Provides utilities for showing authentication status without exposing + sensitive attributes (e.g. hashed_password). + """ + + @doc """ + Returns whether the user has password authentication set. + + Only returns true when `hashed_password` is a non-empty string. This avoids + treating `nil`, empty string, or forbidden/redacted values (e.g. when the + attribute is not visible to the actor) as "has password". + + ## Examples + + iex> user = %{hashed_password: nil} + iex> MvWeb.Helpers.UserHelpers.has_password?(user) + false + + iex> user = %{hashed_password: "$2b$12$..."} + iex> MvWeb.Helpers.UserHelpers.has_password?(user) + true + + iex> user = %{hashed_password: ""} + iex> MvWeb.Helpers.UserHelpers.has_password?(user) + false + """ + @spec has_password?(map() | struct()) :: boolean() + def has_password?(user) when is_map(user) do + case Map.get(user, :hashed_password) do + hash when is_binary(hash) and byte_size(hash) > 0 -> true + _ -> false + end + end + + @doc """ + Returns whether the user is linked via OIDC/SSO (has a non-empty oidc_id). + + ## Examples + + iex> user = %{oidc_id: nil} + iex> MvWeb.Helpers.UserHelpers.has_oidc?(user) + false + + iex> user = %{oidc_id: "sub-from-rauthy"} + iex> MvWeb.Helpers.UserHelpers.has_oidc?(user) + true + """ + @spec has_oidc?(map() | struct()) :: boolean() + def has_oidc?(user) when is_map(user) do + case Map.get(user, :oidc_id) do + id when is_binary(id) and byte_size(id) > 0 -> true + _ -> false + end + end +end diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex index 1eb3e47..72cc55c 100644 --- a/lib/mv_web/live/user_live/index.ex +++ b/lib/mv_web/live/user_live/index.ex @@ -35,7 +35,7 @@ defmodule MvWeb.UserLive.Index do users = Mv.Accounts.User |> Ash.Query.filter(email != ^Mv.Helpers.SystemActor.system_user_email()) - |> Ash.read!(domain: Mv.Accounts, load: [:member], actor: actor) + |> Ash.read!(domain: Mv.Accounts, load: [:member, :role], actor: actor) sorted = Enum.sort_by(users, & &1.email) diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex index cb945e2..bb5a49d 100644 --- a/lib/mv_web/live/user_live/index.html.heex +++ b/lib/mv_web/live/user_live/index.html.heex @@ -56,11 +56,28 @@ > {user.email} + <:col :let={user} label={gettext("Role")}> + {user.role.name} + <:col :let={user} label={gettext("Linked Member")}> <%= if user.member do %> {MvWeb.Helpers.MemberHelpers.display_name(user.member)} <% else %> - {gettext("No member linked")} + {gettext("No member linked")} + <% end %> + + <:col :let={user} label={gettext("Password")}> + <%= if MvWeb.Helpers.UserHelpers.has_password?(user) do %> + {gettext("Enabled")} + <% else %> + + <% end %> + + <:col :let={user} label={gettext("OIDC")}> + <%= if user.oidc_id do %> + {gettext("Linked")} + <% else %> + <% end %> diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index 5114b74..2f52197 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -55,8 +55,14 @@ defmodule MvWeb.UserLive.Show do <.list> <:item title={gettext("Email")}>{@user.email} + <:item title={gettext("Role")}>{@user.role.name} <:item title={gettext("Password Authentication")}> - {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} + {if MvWeb.Helpers.UserHelpers.has_password?(@user), + do: gettext("Enabled"), + else: gettext("Not enabled")} + + <:item title={gettext("OIDC")}> + {if @user.oidc_id, do: gettext("Linked"), else: gettext("Not linked")} <:item title={gettext("Linked Member")}> <%= if @user.member do %> @@ -79,7 +85,9 @@ defmodule MvWeb.UserLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do actor = current_actor(socket) - user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member], actor: actor) + + user = + Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member, :role], actor: actor) if Mv.Helpers.SystemActor.system_user?(user) do {:ok, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 4ea98e1..b732c4a 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -294,6 +294,7 @@ msgstr "Beschreibung" msgid "Edit User" msgstr "Benutzer*in bearbeiten" +#: lib/mv_web/live/user_live/index.html.heex #: lib/mv_web/live/user_live/show.ex #, elixir-autogen, elixir-format msgid "Enabled" @@ -471,6 +472,7 @@ msgid "Include both letters and numbers" msgstr "Buchstaben und Zahlen verwenden" #: lib/mv_web/live/user_live/form.ex +#: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format msgid "Password" msgstr "Passwort" @@ -1670,6 +1672,8 @@ msgstr "Profil" #: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/user_live/form.ex +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex #, elixir-autogen, elixir-format msgid "Role" msgstr "Rolle" @@ -2312,3 +2316,30 @@ msgstr "Du hast keine Berechtigung, diese Aktion auszuführen." #, elixir-autogen, elixir-format msgid "Select a membership fee type" msgstr "Mitgliedsbeitragstyp auswählen" + +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "Linked" +msgstr "Verknüpft" + +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "OIDC" +msgstr "OIDC" + +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "Not linked" +msgstr "Nicht verknüpft" + +#: lib/mv_web/live/user_live/form.ex +#, elixir-autogen, elixir-format +msgid "SSO / OIDC user" +msgstr "SSO-/OIDC-Benutzer*in" + +#: lib/mv_web/live/user_live/form.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT." +msgstr "Dieser*e Benutzer*in ist per SSO (Single Sign-On) angebunden. Ein hier gesetztes oder geändertes Passwort betrifft nur die Anmeldung mit E-Mail und Passwort in dieser Anwendung. Es ändert nicht das Passwort beim Identity-Provider (z. B. Authentik). Zum Ändern des SSO-Passworts nutzen Sie den Identity-Provider oder die IT Ihrer Organisation." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 483f65f..3c147ba 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -295,6 +295,7 @@ msgstr "" msgid "Edit User" msgstr "" +#: lib/mv_web/live/user_live/index.html.heex #: lib/mv_web/live/user_live/show.ex #, elixir-autogen, elixir-format msgid "Enabled" @@ -472,6 +473,7 @@ msgid "Include both letters and numbers" msgstr "" #: lib/mv_web/live/user_live/form.ex +#: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format msgid "Password" msgstr "" @@ -1671,6 +1673,8 @@ msgstr "" #: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/user_live/form.ex +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex #, elixir-autogen, elixir-format msgid "Role" msgstr "" @@ -2313,3 +2317,30 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Select a membership fee type" msgstr "" + +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "Linked" +msgstr "" + +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "OIDC" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "Not linked" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex +#, elixir-autogen, elixir-format +msgid "SSO / OIDC user" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex +#, elixir-autogen, elixir-format +msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 383dacd..7aad814 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -295,6 +295,7 @@ msgstr "" msgid "Edit User" msgstr "" +#: lib/mv_web/live/user_live/index.html.heex #: lib/mv_web/live/user_live/show.ex #, elixir-autogen, elixir-format msgid "Enabled" @@ -472,6 +473,7 @@ msgid "Include both letters and numbers" msgstr "" #: lib/mv_web/live/user_live/form.ex +#: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format msgid "Password" msgstr "" @@ -1671,6 +1673,8 @@ msgstr "" #: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/show.ex #: lib/mv_web/live/user_live/form.ex +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex #, elixir-autogen, elixir-format msgid "Role" msgstr "" @@ -2313,3 +2317,30 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "Select a membership fee type" msgstr "" + +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Linked" +msgstr "" + +#: lib/mv_web/live/user_live/index.html.heex +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format +msgid "OIDC" +msgstr "" + +#: lib/mv_web/live/user_live/show.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Not linked" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex +#, elixir-autogen, elixir-format +msgid "SSO / OIDC user" +msgstr "" + +#: lib/mv_web/live/user_live/form.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT." +msgstr ""