Users list and show: Role, Password, OIDC columns; UserHelpers

- Index: load :role; columns Role, Password (has_password?), OIDC; contrast fix.
- Show: Role, OIDC (Linked/Not linked); has_password? for Password Authentication.
- UserHelpers: has_password?/1, has_oidc?/1. Gettext: new strings and DE translations.
This commit is contained in:
Moritz 2026-02-04 11:06:52 +01:00
parent 7eba21dc9c
commit c6082f2831
7 changed files with 180 additions and 4 deletions

View file

@ -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

View file

@ -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)

View file

@ -56,11 +56,28 @@
>
{user.email}
</:col>
<:col :let={user} label={gettext("Role")}>
{user.role.name}
</:col>
<:col :let={user} label={gettext("Linked Member")}>
<%= if user.member do %>
{MvWeb.Helpers.MemberHelpers.display_name(user.member)}
<% else %>
<span class="text-base-content/50">{gettext("No member linked")}</span>
<span class="text-base-content/70">{gettext("No member linked")}</span>
<% end %>
</:col>
<:col :let={user} label={gettext("Password")}>
<%= if MvWeb.Helpers.UserHelpers.has_password?(user) do %>
<span>{gettext("Enabled")}</span>
<% else %>
<span class="text-base-content/70">—</span>
<% end %>
</:col>
<:col :let={user} label={gettext("OIDC")}>
<%= if user.oidc_id do %>
<span>{gettext("Linked")}</span>
<% else %>
<span class="text-base-content/70">—</span>
<% end %>
</:col>

View file

@ -55,8 +55,14 @@ defmodule MvWeb.UserLive.Show do
<.list>
<:item title={gettext("Email")}>{@user.email}</:item>
<:item title={gettext("Role")}>{@user.role.name}</:item>
<: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>
<:item title={gettext("OIDC")}>
{if @user.oidc_id, do: gettext("Linked"), else: gettext("Not linked")}
</:item>
<: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,