Merge pull request 'feature/account_view closes #106' (#109) from feature/account_view into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #109
Reviewed-by: carla <carla@noreply.git.local-it.org>
This commit is contained in:
moritz 2025-07-24 17:08:33 +02:00
commit 25473afc43
14 changed files with 1697 additions and 54 deletions

View file

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

View file

@ -63,6 +63,27 @@ 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
# 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
@ -75,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
@ -91,11 +112,19 @@ 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
# 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

View file

@ -0,0 +1,180 @@
defmodule MvWeb.UserLive.Form do
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<.header>
{@page_title}
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
</.header>
<.form for={@form} id="user-form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
<!-- Password Section -->
<div class="mt-6">
<label class="flex items-center space-x-2">
<input
type="checkbox"
name="set_password"
phx-click="toggle_password_section"
checked={@show_password_fields}
class="checkbox checkbox-sm"
/>
<span class="text-sm font-medium">
{if @user, do: gettext("Change Password"), else: gettext("Set Password")}
</span>
</label>
<%= if @show_password_fields do %>
<div class="mt-4 space-y-4 p-4 bg-gray-50 rounded-lg">
<.input
field={@form[:password]}
label={gettext("Password")}
type="password"
required
autocomplete="new-password"
/>
<!-- Only show password confirmation for new users (register_with_password) -->
<%= if !@user do %>
<.input
field={@form[:password_confirmation]}
label={gettext("Confirm Password")}
type="password"
required
autocomplete="new-password"
/>
<% end %>
<div class="text-sm text-gray-600">
<p><strong>{gettext("Password requirements")}:</strong></p>
<ul class="list-disc list-inside text-xs mt-1 space-y-1">
<li>{gettext("At least 8 characters")}</li>
<li>{gettext("Include both letters and numbers")}</li>
<li>{gettext("Consider using special characters")}</li>
</ul>
</div>
<%= if @user do %>
<div class="mt-3 p-3 bg-orange-50 border border-orange-200 rounded">
<p class="text-sm text-orange-800">
<strong>{gettext("Admin Note")}:</strong> {gettext(
"As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
)}
</p>
</div>
<% end %>
</div>
<% else %>
<%= if @user do %>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-800">
<strong>{gettext("Note")}:</strong> {gettext(
"Check 'Change Password' above to set a new password for this user."
)}
</p>
</div>
<% else %>
<div class="mt-4 p-4 bg-yellow-50 rounded-lg">
<p class="text-sm text-yellow-800">
<strong>{gettext("Note")}:</strong> {gettext(
"User will be created without a password. Check 'Set Password' to add one."
)}
</p>
</div>
<% end %>
<% end %>
</div>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save User")}
</.button>
<.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")}</.button>
</.form>
</Layouts.app>
"""
end
@impl true
def mount(params, _session, socket) do
user =
case params["id"] do
nil -> nil
id -> Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
end
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
page_title = action <> " " <> gettext("User")
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(user: user)
|> assign(:page_title, page_title)
|> assign(:show_password_fields, false)
|> assign_form()}
end
defp return_to("show"), do: "show"
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
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, show_password_fields: show_password_fields}} = socket) do
form =
if user do
# 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
# 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"
)
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

View file

@ -0,0 +1,86 @@
defmodule MvWeb.UserLive.Index do
use MvWeb, :live_view
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"))
|> assign(:sort_field, :email)
|> assign(:sort_order, :asc)
|> assign(:users, sorted)
|> assign(:selected_users, [])}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
Ash.destroy!(user, domain: Mv.Accounts)
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

View file

@ -0,0 +1,71 @@
<Layouts.app flash={@flash}>
<.header>
{gettext("Listing Users")}
<:actions>
<.button variant="primary" navigate={~p"/users/new"}>
<.icon name="hero-plus" /> {gettext("New User")}
</.button>
</:actions>
</.header>
<.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>
<:col
:let={user}
label={
sort_button(%{
field: :email,
label: gettext("Email"),
sort_field: @sort_field,
sort_order: @sort_order
})
}
>
{user.email}
</:col>
<:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id}</:col>
<:action :let={user}>
<div class="sr-only">
<.link navigate={~p"/users/#{user}"}>{gettext("Show")}</.link>
</div>
<.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")}</.link>
</:action>
<: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")}
</.link>
</:action>
</.table>
</Layouts.app>

View file

@ -0,0 +1,41 @@
defmodule MvWeb.UserLive.Show do
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<.header>
{gettext("User")} {@user.email}
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
<:actions>
<.button navigate={~p"/users"}>
<.icon name="hero-arrow-left" />
</.button>
<.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> {gettext("Edit User")}
</.button>
</:actions>
</.header>
<.list>
<:item title={gettext("ID")}>{@user.id}</:item>
<:item title={gettext("Email")}>{@user.email}</:item>
<:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")}</:item>
<:item title={gettext("Password Authentication")}>
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
{:ok,
socket
|> assign(:page_title, gettext("Show User"))
|> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))}
end
end

View file

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

View file

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

View file

@ -13,9 +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.html.heex:69
#, 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.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:109
#: lib/mv_web/live/user_live/index.html.heex:63
#, 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.html.heex:48
#: 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.html.heex:60
#, 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:92
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr "Speichern..."
@ -217,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
@ -227,64 +236,86 @@ 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: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
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 ""
msgstr "Unveränderlich"
#: lib/mv_web/components/layouts/navbar.ex:35
#: 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:12
#: lib/mv_web/live/user_live/index.html.heex:3
#, 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"
msgstr ""
msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:14
#: lib/mv_web/live/member_live/index.ex:12
@ -297,17 +328,49 @@ 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.html.heex:6
#, 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: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.html.heex:56
#: 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/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:31
#: 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
@ -317,47 +380,62 @@ 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
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:34
#: 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:93
#, 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}"
msgstr ""
msgstr "Nicht unterstützter Wertetyp: %{type}"
#: lib/mv_web/live/property_live/form.ex:10
#, elixir-autogen, elixir-format, fuzzy
@ -369,22 +447,108 @@ 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:110
#: 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"
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
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: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 "Change Password"
msgstr "Passwort ändern"
#: 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 "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diesen Benutzer zu setzen."
#: lib/mv_web/live/user_live/form.ex:45
#, elixir-autogen, elixir-format
msgid "Confirm Password"
msgstr "Passwort bestätigen"
#: lib/mv_web/live/user_live/form.ex:57
#, elixir-autogen, elixir-format
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:53
#, elixir-autogen, elixir-format
msgid "Password requirements"
msgstr "Passwort-Anforderungen"
#: lib/mv_web/live/user_live/index.html.heex:25
#, elixir-autogen, elixir-format
msgid "Select all users"
msgstr "Alle Benutzer auswählen"
#: lib/mv_web/live/user_live/index.html.heex:39
#, elixir-autogen, elixir-format
msgid "Select user"
msgstr "Benutzer auswählen"
#: lib/mv_web/live/user_live/form.ex:27
#, elixir-autogen, elixir-format
msgid "Set Password"
msgstr "Passwort setzen"
#: 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."

View file

@ -17,6 +17,7 @@ msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/user_live/index.html.heex:69
#, 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.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:109
#: lib/mv_web/live/user_live/index.html.heex:63
#, 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.html.heex:48
#: 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.html.heex:60
#, 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:92
#, 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:95
#, 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:12
#: lib/mv_web/live/user_live/index.html.heex:3
#, 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,44 @@ msgstr ""
msgid "Name"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:6
#, 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:75
#: lib/mv_web/live/user_live/form.ex:83
#, elixir-autogen, elixir-format
msgid "Note"
msgstr ""
#: 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"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "Password Authentication"
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 +413,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:93
#, 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 +448,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:110
#: 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 +478,78 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "descending"
msgstr ""
#: 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 "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 ""

View file

@ -17,6 +17,7 @@ msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/user_live/index.html.heex:69
#, 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.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:109
#: lib/mv_web/live/user_live/index.html.heex:63
#, 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.html.heex:48
#: 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.html.heex:60
#, 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:92
#, 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:95
#, 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:12
#: lib/mv_web/live/user_live/index.html.heex:3
#, 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,44 @@ msgstr ""
msgid "Name"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:6
#, 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: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.html.heex:56
#: 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/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 +413,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:93
#, 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 +448,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:110
#: 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 +478,78 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "descending"
msgstr ""
#: 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 "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 "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:53
#, elixir-autogen, elixir-format
msgid "Password requirements"
msgstr "Password requirements"
#: 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/index.html.heex:39
#, elixir-autogen, elixir-format, fuzzy
msgid "Select user"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:27
#, elixir-autogen, elixir-format
msgid "Set Password"
msgstr "Set Password"
#: 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."

View file

@ -0,0 +1,284 @@
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 &#39;Change Password&#39; 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

View file

@ -0,0 +1,412 @@
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)
# Table header should still be there
assert html =~ "Email"
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
# Checking that aria-label attributes exist
assert html =~ ~s(aria-label=)
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

View file

@ -33,16 +33,57 @@ 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])
Ash.Seed.seed!(Mv.Accounts.User, %{
email: email,
hashed_password: hashed_password
})
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
# 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
@doc """
@ -57,8 +98,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