Complete Permissions for Groups, Membership Fees, and User Role Assignment closes #404 #405

Merged
moritz merged 26 commits from feature/404_permission_completeness into main 2026-02-04 11:47:19 +01:00
7 changed files with 180 additions and 4 deletions
Showing only changes of commit c6082f2831 - Show all commits

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,

View file

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

View file

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

View file

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