Compare commits
27 commits
b48529113c
...
74f398e58e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f398e58e | ||
| 36b7031dca | |||
|
|
fa5afba6ba | ||
| 0c313824fb | |||
|
|
f45ae66f18 | ||
| c2bafe4acf | |||
| cbc9376b7b | |||
| ee6bfbacbb | |||
| a4b13cef49 | |||
| 286972964d | |||
| c36812bf3f | |||
| 2ddd22078d | |||
| 9e8910344e | |||
| 1426ef1d38 | |||
| f779fd61e0 | |||
| cc9e530d80 | |||
| 2f67c7099d | |||
| 5e361ba400 | |||
| 505e31653a | |||
| d3ad7c5013 | |||
| 131904f172 | |||
| 47b6a16177 | |||
| 60a4181255 | |||
| 4e6b7305b6 | |||
| 4ea31f0f37 | |||
| ad02f8914f | |||
| 3d46ba655f |
26 changed files with 907 additions and 141 deletions
|
|
@ -273,7 +273,7 @@ environment:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: renovate
|
- name: renovate
|
||||||
image: renovate/renovate:42.81
|
image: renovate/renovate:42.95
|
||||||
environment:
|
environment:
|
||||||
RENOVATE_CONFIG_FILE: "renovate_backend_config.js"
|
RENOVATE_CONFIG_FILE: "renovate_backend_config.js"
|
||||||
RENOVATE_TOKEN:
|
RENOVATE_TOKEN:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ services:
|
||||||
|
|
||||||
rauthy:
|
rauthy:
|
||||||
container_name: rauthy-dev
|
container_name: rauthy-dev
|
||||||
image: ghcr.io/sebadob/rauthy:0.33.4
|
image: ghcr.io/sebadob/rauthy:0.34.2
|
||||||
environment:
|
environment:
|
||||||
- LOCAL_TEST=true
|
- LOCAL_TEST=true
|
||||||
- SMTP_URL=mailcrab
|
- SMTP_URL=mailcrab
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
2. **DB constraints** - Prevent duplicates within same table (users.email, members.email)
|
2. **DB constraints** - Prevent duplicates within same table (users.email, members.email)
|
||||||
3. **Custom validations** - Prevent cross-table conflicts only for linked entities
|
3. **Custom validations** - Prevent cross-table conflicts only for linked entities
|
||||||
4. **Sync is bidirectional**: User ↔ Member (but User always wins on link)
|
4. **Sync is bidirectional**: User ↔ Member (but User always wins on link)
|
||||||
|
5. **Linked member email change** - When a member is linked, only administrators or the linked user may change that member's email (Member resource validation `EmailChangePermission`). Because User.email wins on link and changes sync Member → User, allowing anyone to change a linked member's email would overwrite that user's account email; this rule keeps sync under control.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ defmodule Mv.Membership.Member do
|
||||||
- Postal code format: exactly 5 digits (German format)
|
- Postal code format: exactly 5 digits (German format)
|
||||||
- Date validations: join_date not in future, exit_date after join_date
|
- Date validations: join_date not in future, exit_date after join_date
|
||||||
- Email uniqueness: prevents conflicts with unlinked users
|
- Email uniqueness: prevents conflicts with unlinked users
|
||||||
|
- Linked member email change: only admins or the linked user may change a linked member's email (see `Mv.Membership.Member.Validations.EmailChangePermission`)
|
||||||
|
|
||||||
## Full-Text Search
|
## Full-Text Search
|
||||||
Members have a `search_vector` attribute (tsvector) that is automatically
|
Members have a `search_vector` attribute (tsvector) that is automatically
|
||||||
|
|
@ -381,6 +382,9 @@ defmodule Mv.Membership.Member do
|
||||||
# Validates that member email is not already used by another (unlinked) user
|
# Validates that member email is not already used by another (unlinked) user
|
||||||
validate Mv.Membership.Member.Validations.EmailNotUsedByOtherUser
|
validate Mv.Membership.Member.Validations.EmailNotUsedByOtherUser
|
||||||
|
|
||||||
|
# Only admins or the linked user may change a linked member's email (prevents breaking sync)
|
||||||
|
validate Mv.Membership.Member.Validations.EmailChangePermission, on: [:update]
|
||||||
|
|
||||||
# Prevent linking to a user that already has a member
|
# Prevent linking to a user that already has a member
|
||||||
# This validation prevents "stealing" users from other members by checking
|
# This validation prevents "stealing" users from other members by checking
|
||||||
# if the target user is already linked to a different member
|
# if the target user is already linked to a different member
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Mv.Authorization.Actor do
|
defmodule Mv.Authorization.Actor do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Helper functions for ensuring User actors have required data loaded.
|
Helper functions for ensuring User actors have required data loaded
|
||||||
|
and for querying actor capabilities (e.g. admin, permission set).
|
||||||
|
|
||||||
## Actor Invariant
|
## Actor Invariant
|
||||||
|
|
||||||
|
|
@ -27,8 +28,11 @@ defmodule Mv.Authorization.Actor do
|
||||||
assign(socket, :current_user, user)
|
assign(socket, :current_user, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
# In tests
|
# Check if actor is admin (policy checks, validations)
|
||||||
user = Actor.ensure_loaded(user)
|
if Actor.admin?(actor), do: ...
|
||||||
|
|
||||||
|
# Get permission set name (string or nil)
|
||||||
|
ps_name = Actor.permission_set_name(actor)
|
||||||
|
|
||||||
## Security Note
|
## Security Note
|
||||||
|
|
||||||
|
|
@ -47,6 +51,8 @@ defmodule Mv.Authorization.Actor do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
alias Mv.Helpers.SystemActor
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Ensures the actor (User) has their `:role` relationship loaded.
|
Ensures the actor (User) has their `:role` relationship loaded.
|
||||||
|
|
||||||
|
|
@ -96,4 +102,45 @@ defmodule Mv.Authorization.Actor do
|
||||||
actor
|
actor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the actor's permission set name (string or atom) from their role, or nil.
|
||||||
|
|
||||||
|
Ensures role is loaded (including when role is nil). Supports both atom and
|
||||||
|
string keys for session/socket assigns. Use for capability checks consistent
|
||||||
|
with `ActorIsAdmin` and `HasPermission`.
|
||||||
|
"""
|
||||||
|
@spec permission_set_name(Mv.Accounts.User.t() | map() | nil) :: String.t() | atom() | nil
|
||||||
|
def permission_set_name(nil), do: nil
|
||||||
|
|
||||||
|
def permission_set_name(actor) do
|
||||||
|
actor = actor |> ensure_loaded() |> maybe_load_role()
|
||||||
|
|
||||||
|
get_in(actor, [Access.key(:role), Access.key(:permission_set_name)]) ||
|
||||||
|
get_in(actor, [Access.key("role"), Access.key("permission_set_name")])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns true if the actor is the system user or has the admin permission set.
|
||||||
|
|
||||||
|
Use for validations and policy checks that require admin capability (e.g.
|
||||||
|
changing a linked member's email). Consistent with `ActorIsAdmin` policy check.
|
||||||
|
"""
|
||||||
|
@spec admin?(Mv.Accounts.User.t() | map() | nil) :: boolean()
|
||||||
|
def admin?(nil), do: false
|
||||||
|
|
||||||
|
def admin?(actor) do
|
||||||
|
SystemActor.system_user?(actor) or permission_set_name(actor) in ["admin", :admin]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load role only when it is nil (e.g. actor from session without role). ensure_loaded/1
|
||||||
|
# already handles %Ash.NotLoaded{}, so we do not double-load in the normal Ash path.
|
||||||
|
defp maybe_load_role(%Mv.Accounts.User{role: nil} = user) do
|
||||||
|
case Ash.load(user, :role, domain: Mv.Accounts, authorize?: false) do
|
||||||
|
{:ok, loaded} -> loaded
|
||||||
|
_ -> user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_load_role(actor), do: actor
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
defmodule Mv.Authorization.Checks.ActorIsAdmin do
|
defmodule Mv.Authorization.Checks.ActorIsAdmin do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Policy check: true when the actor's role has permission_set_name "admin".
|
Policy check: true when the actor is the system user or has permission_set_name "admin".
|
||||||
|
|
||||||
Used to restrict actions (e.g. User.update_user for member link/unlink) to admins only.
|
Used to restrict actions (e.g. User.update_user for member link/unlink) to admins only.
|
||||||
|
Delegates to `Mv.Authorization.Actor.admin?/1`, which returns true for the system actor
|
||||||
|
or for a user whose role has permission_set_name "admin".
|
||||||
"""
|
"""
|
||||||
use Ash.Policy.SimpleCheck
|
use Ash.Policy.SimpleCheck
|
||||||
|
|
||||||
|
alias Mv.Authorization.Actor
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def describe(_opts), do: "actor has admin permission set"
|
def describe(_opts), do: "actor has admin permission set"
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def match?(nil, _context, _opts), do: false
|
def match?(actor, _context, _opts), do: Actor.admin?(actor)
|
||||||
|
|
||||||
def match?(actor, _context, _opts) do
|
|
||||||
ps_name =
|
|
||||||
get_in(actor, [Access.key(:role), Access.key(:permission_set_name)]) ||
|
|
||||||
get_in(actor, [Access.key("role"), Access.key("permission_set_name")])
|
|
||||||
|
|
||||||
ps_name == "admin"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@ defmodule Mv.EmailSync.Loader do
|
||||||
Helper functions for loading linked records in email synchronization.
|
Helper functions for loading linked records in email synchronization.
|
||||||
Centralizes the logic for retrieving related User/Member entities.
|
Centralizes the logic for retrieving related User/Member entities.
|
||||||
|
|
||||||
## Authorization
|
## Authorization-independent link checks
|
||||||
|
|
||||||
This module runs systemically and uses the system actor for all operations.
|
All functions use the **system actor** for the load. Link existence
|
||||||
This ensures that email synchronization always works, regardless of user permissions.
|
(linked vs not linked) is therefore determined **independently of the
|
||||||
|
current request actor**. This is required so that validations (e.g.
|
||||||
All functions use `Mv.Helpers.SystemActor.get_system_actor/0` to bypass
|
`EmailChangePermission`, `EmailNotUsedByOtherUser`) can correctly decide
|
||||||
user permission checks, as email sync is a mandatory side effect.
|
"member is linked" even when the current user would not have read permission
|
||||||
|
on the related User. Using the request actor would otherwise allow
|
||||||
|
treating a linked member as unlinked and bypass the permission rule.
|
||||||
"""
|
"""
|
||||||
alias Mv.Helpers
|
alias Mv.Helpers
|
||||||
alias Mv.Helpers.SystemActor
|
alias Mv.Helpers.SystemActor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
defmodule Mv.Membership.Member.Validations.EmailChangePermission do
|
||||||
|
@moduledoc """
|
||||||
|
Validates that only admins or the linked user may change a linked member's email.
|
||||||
|
|
||||||
|
This validation runs on member update when the email attribute is changing.
|
||||||
|
It allows the change only if:
|
||||||
|
- The member is not linked to a user, or
|
||||||
|
- The actor has the admin permission set (via `Mv.Authorization.Actor.admin?/1`), or
|
||||||
|
- The actor is the user linked to this member (actor.member_id == member.id).
|
||||||
|
|
||||||
|
This prevents non-admins from changing another user's linked member email,
|
||||||
|
which would sync to that user's account and break email synchronization.
|
||||||
|
|
||||||
|
Missing actor is not allowed; the system actor counts as admin (via `Actor.admin?/1`).
|
||||||
|
"""
|
||||||
|
use Ash.Resource.Validation
|
||||||
|
use Gettext, backend: MvWeb.Gettext, otp_app: :mv
|
||||||
|
|
||||||
|
alias Mv.Authorization.Actor
|
||||||
|
alias Mv.EmailSync.Loader
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Validates that the actor may change the member's email when the member is linked.
|
||||||
|
|
||||||
|
Only runs when the email attribute is changing (checked inside). Skips when
|
||||||
|
member is not linked. Allows when actor is admin or owns the linked member.
|
||||||
|
"""
|
||||||
|
@impl true
|
||||||
|
def validate(changeset, _opts, context) do
|
||||||
|
if Ash.Changeset.changing_attribute?(changeset, :email) do
|
||||||
|
validate_linked_member_email_change(changeset, context)
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_linked_member_email_change(changeset, context) do
|
||||||
|
linked_user = Loader.get_linked_user(changeset.data)
|
||||||
|
|
||||||
|
if is_nil(linked_user) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
actor = resolve_actor(changeset, context)
|
||||||
|
member_id = changeset.data.id
|
||||||
|
|
||||||
|
if Actor.admin?(actor) or actor_owns_member?(actor, member_id) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
msg =
|
||||||
|
dgettext(
|
||||||
|
"default",
|
||||||
|
"Only administrators or the linked user can change the email for members linked to users"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error, field: :email, message: msg}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ash stores actor in changeset.context.private.actor; validation context has .actor; some callsites use context.actor
|
||||||
|
defp resolve_actor(changeset, context) do
|
||||||
|
ctx = changeset.context || %{}
|
||||||
|
|
||||||
|
get_in(ctx, [:private, :actor]) ||
|
||||||
|
Map.get(ctx, :actor) ||
|
||||||
|
(context && Map.get(context, :actor))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_owns_member?(nil, _member_id), do: false
|
||||||
|
|
||||||
|
defp actor_owns_member?(actor, member_id) do
|
||||||
|
actor_member_id = Map.get(actor, :member_id) || Map.get(actor, "member_id")
|
||||||
|
actor_member_id == member_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -8,6 +8,8 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
|
||||||
This allows creating members with the same email as unlinked users.
|
This allows creating members with the same email as unlinked users.
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Validation
|
use Ash.Resource.Validation
|
||||||
|
|
||||||
|
alias Mv.EmailSync.Loader
|
||||||
alias Mv.Helpers
|
alias Mv.Helpers
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
@ -32,7 +34,8 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
|
||||||
def validate(changeset, _opts, _context) do
|
def validate(changeset, _opts, _context) do
|
||||||
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
|
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
|
||||||
|
|
||||||
linked_user_id = get_linked_user_id(changeset.data)
|
linked_user = Loader.get_linked_user(changeset.data)
|
||||||
|
linked_user_id = if linked_user, do: linked_user.id, else: nil
|
||||||
is_linked? = not is_nil(linked_user_id)
|
is_linked? = not is_nil(linked_user_id)
|
||||||
|
|
||||||
# Only validate if member is already linked AND email is changing
|
# Only validate if member is already linked AND email is changing
|
||||||
|
|
@ -76,16 +79,4 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
|
||||||
|
|
||||||
defp maybe_exclude_id(query, nil), do: query
|
defp maybe_exclude_id(query, nil), do: query
|
||||||
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
|
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
|
||||||
|
|
||||||
defp get_linked_user_id(member_data) do
|
|
||||||
alias Mv.Helpers.SystemActor
|
|
||||||
|
|
||||||
system_actor = SystemActor.get_system_actor()
|
|
||||||
opts = Helpers.ash_actor_opts(system_actor)
|
|
||||||
|
|
||||||
case Ash.load(member_data, :user, opts) do
|
|
||||||
{:ok, %{user: %{id: id}}} -> id
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,18 @@ defmodule MvWeb.Authorization do
|
||||||
@doc """
|
@doc """
|
||||||
Checks if user can access a specific page.
|
Checks if user can access a specific page.
|
||||||
|
|
||||||
|
Nil-safe: returns false when user is nil (e.g. unauthenticated or layout
|
||||||
|
assigns regression), so callers do not need to guard.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> admin = %{role: %{permission_set_name: "admin"}}
|
iex> admin = %{role: %{permission_set_name: "admin"}}
|
||||||
iex> can_access_page?(admin, "/admin/roles")
|
iex> can_access_page?(admin, "/admin/roles")
|
||||||
true
|
true
|
||||||
|
|
||||||
|
iex> can_access_page?(nil, "/members")
|
||||||
|
false
|
||||||
|
|
||||||
iex> mitglied = %{role: %{permission_set_name: "own_data"}}
|
iex> mitglied = %{role: %{permission_set_name: "own_data"}}
|
||||||
iex> can_access_page?(mitglied, "/members")
|
iex> can_access_page?(mitglied, "/members")
|
||||||
false
|
false
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,13 @@ defmodule MvWeb.CoreComponents do
|
||||||
<.button navigate={~p"/"}>Home</.button>
|
<.button navigate={~p"/"}>Home</.button>
|
||||||
<.button disabled={true}>Disabled</.button>
|
<.button disabled={true}>Disabled</.button>
|
||||||
"""
|
"""
|
||||||
attr :rest, :global, include: ~w(href navigate patch method)
|
attr :rest, :global, include: ~w(href navigate patch method data-testid)
|
||||||
attr :variant, :string, values: ~w(primary)
|
attr :variant, :string, values: ~w(primary)
|
||||||
attr :disabled, :boolean, default: false, doc: "Whether the button is disabled"
|
attr :disabled, :boolean, default: false, doc: "Whether the button is disabled"
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
def button(%{rest: rest} = assigns) do
|
def button(assigns) do
|
||||||
|
rest = assigns.rest
|
||||||
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
||||||
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))
|
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ defmodule MvWeb.Layouts.Sidebar do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :html
|
use MvWeb, :html
|
||||||
|
|
||||||
|
alias MvWeb.PagePaths
|
||||||
|
|
||||||
attr :current_user, :map, default: nil, doc: "The current user"
|
attr :current_user, :map, default: nil, doc: "The current user"
|
||||||
attr :club_name, :string, required: true, doc: "The name of the club"
|
attr :club_name, :string, required: true, doc: "The name of the club"
|
||||||
attr :mobile, :boolean, default: false, doc: "Whether this is mobile view"
|
attr :mobile, :boolean, default: false, doc: "Whether this is mobile view"
|
||||||
|
|
@ -70,33 +72,56 @@ defmodule MvWeb.Layouts.Sidebar do
|
||||||
defp sidebar_menu(assigns) do
|
defp sidebar_menu(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<ul class="menu flex-1 w-full p-2" role="menubar">
|
<ul class="menu flex-1 w-full p-2" role="menubar">
|
||||||
<.menu_item
|
<%= if can_access_page?(@current_user, PagePaths.members()) do %>
|
||||||
href={~p"/members"}
|
<.menu_item
|
||||||
icon="hero-users"
|
href={~p"/members"}
|
||||||
label={gettext("Members")}
|
icon="hero-users"
|
||||||
/>
|
label={gettext("Members")}
|
||||||
|
|
||||||
<.menu_item
|
|
||||||
href={~p"/membership_fee_types"}
|
|
||||||
icon="hero-currency-euro"
|
|
||||||
label={gettext("Fee Types")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Nested Admin Menu -->
|
|
||||||
<.menu_group icon="hero-cog-6-tooth" label={gettext("Administration")}>
|
|
||||||
<.menu_subitem href={~p"/users"} label={gettext("Users")} />
|
|
||||||
<.menu_subitem href={~p"/groups"} label={gettext("Groups")} />
|
|
||||||
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
|
|
||||||
<.menu_subitem
|
|
||||||
href={~p"/membership_fee_settings"}
|
|
||||||
label={gettext("Fee Settings")}
|
|
||||||
/>
|
/>
|
||||||
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} />
|
<% end %>
|
||||||
</.menu_group>
|
|
||||||
|
<%= if can_access_page?(@current_user, PagePaths.membership_fee_types()) do %>
|
||||||
|
<.menu_item
|
||||||
|
href={~p"/membership_fee_types"}
|
||||||
|
icon="hero-currency-euro"
|
||||||
|
label={gettext("Fee Types")}
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if admin_menu_visible?(@current_user) do %>
|
||||||
|
<.menu_group
|
||||||
|
icon="hero-cog-6-tooth"
|
||||||
|
label={gettext("Administration")}
|
||||||
|
testid="sidebar-administration"
|
||||||
|
>
|
||||||
|
<%= if can_access_page?(@current_user, PagePaths.users()) do %>
|
||||||
|
<.menu_subitem href={~p"/users"} label={gettext("Users")} />
|
||||||
|
<% end %>
|
||||||
|
<%= if can_access_page?(@current_user, PagePaths.groups()) do %>
|
||||||
|
<.menu_subitem href={~p"/groups"} label={gettext("Groups")} />
|
||||||
|
<% end %>
|
||||||
|
<%= if can_access_page?(@current_user, PagePaths.admin_roles()) do %>
|
||||||
|
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
|
||||||
|
<% end %>
|
||||||
|
<%= if can_access_page?(@current_user, PagePaths.membership_fee_settings()) do %>
|
||||||
|
<.menu_subitem
|
||||||
|
href={~p"/membership_fee_settings"}
|
||||||
|
label={gettext("Fee Settings")}
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
<%= if can_access_page?(@current_user, PagePaths.settings()) do %>
|
||||||
|
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} />
|
||||||
|
<% end %>
|
||||||
|
</.menu_group>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp admin_menu_visible?(user) do
|
||||||
|
Enum.any?(PagePaths.admin_menu_paths(), &can_access_page?(user, &1))
|
||||||
|
end
|
||||||
|
|
||||||
attr :href, :string, required: true, doc: "Navigation path"
|
attr :href, :string, required: true, doc: "Navigation path"
|
||||||
attr :icon, :string, required: true, doc: "Heroicon name"
|
attr :icon, :string, required: true, doc: "Heroicon name"
|
||||||
attr :label, :string, required: true, doc: "Menu item label"
|
attr :label, :string, required: true, doc: "Menu item label"
|
||||||
|
|
@ -119,12 +144,13 @@ defmodule MvWeb.Layouts.Sidebar do
|
||||||
|
|
||||||
attr :icon, :string, required: true, doc: "Heroicon name for the menu group"
|
attr :icon, :string, required: true, doc: "Heroicon name for the menu group"
|
||||||
attr :label, :string, required: true, doc: "Menu group label"
|
attr :label, :string, required: true, doc: "Menu group label"
|
||||||
|
attr :testid, :string, default: nil, doc: "data-testid for stable test selectors"
|
||||||
slot :inner_block, required: true, doc: "Submenu items"
|
slot :inner_block, required: true, doc: "Submenu items"
|
||||||
|
|
||||||
defp menu_group(assigns) do
|
defp menu_group(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<!-- Expanded Mode: Always open div structure -->
|
<!-- Expanded Mode: Always open div structure -->
|
||||||
<li role="none" class="expanded-menu-group">
|
<li role="none" class="expanded-menu-group" data-testid={@testid}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3"
|
class="flex items-center gap-3"
|
||||||
role="group"
|
role="group"
|
||||||
|
|
@ -138,7 +164,7 @@ defmodule MvWeb.Layouts.Sidebar do
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<!-- Collapsed Mode: Dropdown -->
|
<!-- Collapsed Mode: Dropdown -->
|
||||||
<div class="collapsed-menu-group dropdown dropdown-right">
|
<div class="collapsed-menu-group dropdown dropdown-right" data-testid={@testid}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@
|
||||||
<.icon name="hero-envelope" />
|
<.icon name="hero-envelope" />
|
||||||
{gettext("Open in email program")}
|
{gettext("Open in email program")}
|
||||||
</.button>
|
</.button>
|
||||||
<.button variant="primary" navigate={~p"/members/new"}>
|
<%= if can?(@current_user, :create, Mv.Membership.Member) do %>
|
||||||
<.icon name="hero-plus" /> {gettext("New Member")}
|
<.button variant="primary" navigate={~p"/members/new"} data-testid="member-new">
|
||||||
</.button>
|
<.icon name="hero-plus" /> {gettext("New Member")}
|
||||||
|
</.button>
|
||||||
|
<% end %>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
|
@ -84,6 +86,7 @@
|
||||||
<.table
|
<.table
|
||||||
id="members"
|
id="members"
|
||||||
rows={@members}
|
rows={@members}
|
||||||
|
row_id={fn member -> "row-#{member.id}" end}
|
||||||
row_click={fn member -> JS.navigate(~p"/members/#{member}") end}
|
row_click={fn member -> JS.navigate(~p"/members/#{member}") end}
|
||||||
dynamic_cols={@dynamic_cols}
|
dynamic_cols={@dynamic_cols}
|
||||||
sort_field={@sort_field}
|
sort_field={@sort_field}
|
||||||
|
|
@ -297,16 +300,23 @@
|
||||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")}</.link>
|
<%= if can?(@current_user, :update, member) do %>
|
||||||
|
<.link navigate={~p"/members/#{member}/edit"} data-testid="member-edit">
|
||||||
|
{gettext("Edit")}
|
||||||
|
</.link>
|
||||||
|
<% end %>
|
||||||
</:action>
|
</:action>
|
||||||
|
|
||||||
<:action :let={member}>
|
<:action :let={member}>
|
||||||
<.link
|
<%= if can?(@current_user, :destroy, member) do %>
|
||||||
phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")}
|
<.link
|
||||||
data-confirm={gettext("Are you sure?")}
|
phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")}
|
||||||
>
|
data-confirm={gettext("Are you sure?")}
|
||||||
{gettext("Delete")}
|
data-testid="member-delete"
|
||||||
</.link>
|
>
|
||||||
|
{gettext("Delete")}
|
||||||
|
</.link>
|
||||||
|
<% end %>
|
||||||
</:action>
|
</:action>
|
||||||
</.table>
|
</.table>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,15 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
|
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}>
|
<%= if can?(@current_user, :update, @member) do %>
|
||||||
{gettext("Edit Member")}
|
<.button
|
||||||
</.button>
|
variant="primary"
|
||||||
|
navigate={~p"/members/#{@member}/edit?return_to=show"}
|
||||||
|
data-testid="member-edit"
|
||||||
|
>
|
||||||
|
{gettext("Edit Member")}
|
||||||
|
</.button>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Tab Navigation --%>
|
<%!-- Tab Navigation --%>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,20 @@
|
||||||
<.header>
|
<.header>
|
||||||
{gettext("Listing Users")}
|
{gettext("Listing Users")}
|
||||||
<:actions>
|
<:actions>
|
||||||
<.button variant="primary" navigate={~p"/users/new"}>
|
<%= if can?(@current_user, :create, Mv.Accounts.User) do %>
|
||||||
<.icon name="hero-plus" /> {gettext("New User")}
|
<.button variant="primary" navigate={~p"/users/new"} data-testid="user-new">
|
||||||
</.button>
|
<.icon name="hero-plus" /> {gettext("New User")}
|
||||||
|
</.button>
|
||||||
|
<% end %>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<.table id="users" rows={@users} row_click={fn user -> JS.navigate(~p"/users/#{user}") end}>
|
<.table
|
||||||
|
id="users"
|
||||||
|
rows={@users}
|
||||||
|
row_id={fn user -> "row-#{user.id}" end}
|
||||||
|
row_click={fn user -> JS.navigate(~p"/users/#{user}") end}
|
||||||
|
>
|
||||||
<:col
|
<:col
|
||||||
:let={user}
|
:let={user}
|
||||||
label={
|
label={
|
||||||
|
|
@ -62,16 +69,23 @@
|
||||||
<.link navigate={~p"/users/#{user}"}>{gettext("Show")}</.link>
|
<.link navigate={~p"/users/#{user}"}>{gettext("Show")}</.link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")}</.link>
|
<%= if can?(@current_user, :update, user) do %>
|
||||||
|
<.link navigate={~p"/users/#{user}/edit"} data-testid="user-edit">
|
||||||
|
{gettext("Edit")}
|
||||||
|
</.link>
|
||||||
|
<% end %>
|
||||||
</:action>
|
</:action>
|
||||||
|
|
||||||
<:action :let={user}>
|
<:action :let={user}>
|
||||||
<.link
|
<%= if can?(@current_user, :destroy, user) do %>
|
||||||
phx-click={JS.push("delete", value: %{id: user.id}) |> hide("#row-#{user.id}")}
|
<.link
|
||||||
data-confirm={gettext("Are you sure?")}
|
phx-click={JS.push("delete", value: %{id: user.id}) |> hide("#row-#{user.id}")}
|
||||||
>
|
data-confirm={gettext("Are you sure?")}
|
||||||
{gettext("Delete")}
|
data-testid="user-delete"
|
||||||
</.link>
|
>
|
||||||
|
{gettext("Delete")}
|
||||||
|
</.link>
|
||||||
|
<% end %>
|
||||||
</:action>
|
</:action>
|
||||||
</.table>
|
</.table>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,15 @@ defmodule MvWeb.UserLive.Show do
|
||||||
<.icon name="hero-arrow-left" />
|
<.icon name="hero-arrow-left" />
|
||||||
<span class="sr-only">{gettext("Back to users list")}</span>
|
<span class="sr-only">{gettext("Back to users list")}</span>
|
||||||
</.button>
|
</.button>
|
||||||
<.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}>
|
<%= if can?(@current_user, :update, @user) do %>
|
||||||
<.icon name="hero-pencil-square" /> {gettext("Edit User")}
|
<.button
|
||||||
</.button>
|
variant="primary"
|
||||||
|
navigate={~p"/users/#{@user}/edit?return_to=show"}
|
||||||
|
data-testid="user-edit"
|
||||||
|
>
|
||||||
|
<.icon name="hero-pencil-square" /> {gettext("Edit User")}
|
||||||
|
</.button>
|
||||||
|
<% end %>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
|
|
||||||
42
lib/mv_web/page_paths.ex
Normal file
42
lib/mv_web/page_paths.ex
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
defmodule MvWeb.PagePaths do
|
||||||
|
@moduledoc """
|
||||||
|
Central path strings for UI authorization and sidebar menu.
|
||||||
|
|
||||||
|
Keep in sync with `MvWeb.Router`. Used by Sidebar and `can_access_page?/2`
|
||||||
|
so route changes (prefix, rename) are updated in one place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Sidebar top-level menu paths
|
||||||
|
@members "/members"
|
||||||
|
@membership_fee_types "/membership_fee_types"
|
||||||
|
|
||||||
|
# Administration submenu paths (all must match router)
|
||||||
|
@users "/users"
|
||||||
|
@groups "/groups"
|
||||||
|
@admin_roles "/admin/roles"
|
||||||
|
@membership_fee_settings "/membership_fee_settings"
|
||||||
|
@settings "/settings"
|
||||||
|
|
||||||
|
@admin_page_paths [
|
||||||
|
@users,
|
||||||
|
@groups,
|
||||||
|
@admin_roles,
|
||||||
|
@membership_fee_settings,
|
||||||
|
@settings
|
||||||
|
]
|
||||||
|
|
||||||
|
@doc "Path for Members index (sidebar and page permission check)."
|
||||||
|
def members, do: @members
|
||||||
|
|
||||||
|
@doc "Path for Membership Fee Types index (sidebar and page permission check)."
|
||||||
|
def membership_fee_types, do: @membership_fee_types
|
||||||
|
|
||||||
|
@doc "Paths for Administration menu; show group if user can access any of these."
|
||||||
|
def admin_menu_paths, do: @admin_page_paths
|
||||||
|
|
||||||
|
def users, do: @users
|
||||||
|
def groups, do: @groups
|
||||||
|
def admin_roles, do: @admin_roles
|
||||||
|
def membership_fee_settings, do: @membership_fee_settings
|
||||||
|
def settings, do: @settings
|
||||||
|
end
|
||||||
40
mix.lock
40
mix.lock
|
|
@ -1,22 +1,22 @@
|
||||||
%{
|
%{
|
||||||
"ash": {:hex, :ash, "3.12.0", "5b78000df650d86b446d88977ef8aa5c9d9f7ffa1193fa3c4b901c60bff2d130", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7cf45b4eb83aa0ab5e6707d6e4ea4a10c29ab20613c87f06344f7953b2ca5e18"},
|
"ash": {:hex, :ash, "3.14.1", "22e0ac5dfd4c7d502bd103f0b4380defd66d7c6c83b3a4f54af7045f13da00d7", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "776a5963790d5af79855ddca1718a037d06b49063a6b97fae9110050b3d5127d"},
|
||||||
"ash_admin": {:hex, :ash_admin, "0.13.24", "4fafddc7b4450a92878b58630688c55cab20b0c27e35cad68f29811f73815816", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "8f298cf6cb6635744ed75dd401295ed52513ea4df169f0f89d6a9a75dc4de4dc"},
|
"ash_admin": {:hex, :ash_admin, "0.13.24", "4fafddc7b4450a92878b58630688c55cab20b0c27e35cad68f29811f73815816", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "8f298cf6cb6635744ed75dd401295ed52513ea4df169f0f89d6a9a75dc4de4dc"},
|
||||||
"ash_authentication": {:hex, :ash_authentication, "4.13.6", "95b17f0bfc00bd6e229145b90c7026f784ae81639e832de4b5c96a738de5ed46", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "27ded84bdc61fd267794dee17a6cbe6e52d0f365d3e8ea0460d95977b82ac6f1"},
|
"ash_authentication": {:hex, :ash_authentication, "4.13.7", "421b5ddb516026f6794435980a632109ec116af2afa68a45e15fb48b41c92cfa", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "0d45ac3fdcca6902dabbe161ce63e9cea8f90583863c2e14261c9309e5837121"},
|
||||||
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.14.1", "60d127a73c2144b39fa3dab045cc3f7fce0c3ccd2dd3e8534288f5da65f0c1db", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [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.1", [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", "3cd57aee855be3ccf2960ce0b005ad209c97fbfc81faa71212bcfbd6a4a90cae"},
|
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.15.0", "89e71e96a3d954aed7ed0c1f511d42cbfd19009b813f580b12749b01bbea5148", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [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.1", [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", "d2da66dcf62bc1054ce8f5d9c2829b1dff1dbc3f1d03f9ef0cbe89123d7df107"},
|
||||||
"ash_phoenix": {:hex, :ash_phoenix, "2.3.18", "fad1b8af1405758888086de568f08650c2911ee97074cfe2e325b14854bc43dd", [: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", "7ec28f9216221e83b90d9c3605e9d1cdd228984e09a1a86c9b9d393cebf25222"},
|
"ash_phoenix": {:hex, :ash_phoenix, "2.3.19", "244b24256a7d730e5223f36f371a95971542a547a12f0fb73406f67977e86c97", [: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", "754a7d869a3961a927abb7ff700af9895d2e69dd3b8f9471b0aa8e859cc4b135"},
|
||||||
"ash_postgres": {:hex, :ash_postgres, "2.6.27", "7aa119cc420909573a51802f414a49a9fb21a06ee78769efd7a4db040e748f5c", [:mix], [{:ash, ">= 3.11.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.16 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.29 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]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "f5e71dc3f77bc0c52374869df4b66493e13c0e27507c3d10ff13158ef7ea506f"},
|
"ash_postgres": {:hex, :ash_postgres, "2.6.29", "93c7d39890930548acc704613b7f83e65c0880940be1b2048ee86dfb44918529", [:mix], [{:ash, "~> 3.14", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.4.3 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.29 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]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "0aed7ac3d8407ff094218b1dc86b88ea7e39249fb9e94360c7dac1711e206d8b"},
|
||||||
"ash_sql": {:hex, :ash_sql, "0.3.16", "a4e62d2cf9b2f4a451067e5e3de28349a8d0e69cf50fc1861bad85f478ded046", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "f3d5a810b23e12e3e102799c68b1e934fa7f909ccaa4bd530f10c7317cfcfe56"},
|
"ash_sql": {:hex, :ash_sql, "0.4.3", "2c74e0a19646e3d31a384a2712fc48a82d04ceea74467771ce496fd64dbb55db", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "b0ecc00502178407e607ae4bcfd2f264f36f6a884218024b5e4d5b3dcfa5e027"},
|
||||||
"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"},
|
"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.10.1", "6b1f8609d947ae2a74da5bba8aee938c94348634e54e5625eef622ca0bbbb062", [: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", "4b4c35f273030e44268ace53bf3d5991dfc385c77374244e2f960876547671aa"},
|
"bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [: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", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"},
|
||||||
"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"},
|
"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"},
|
||||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
|
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||||
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
|
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
|
||||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||||
"credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"},
|
"credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"},
|
||||||
"crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"},
|
"crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"},
|
||||||
"db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"},
|
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
||||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||||
|
|
@ -28,21 +28,21 @@
|
||||||
"ex_phone_number": {:hex, :ex_phone_number, "0.4.8", "c1c5e6f0673822a2a7b439b43af7d3eb1a5c19ae582b772b8b8d12625dd51ec1", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "43e2357c6b8cfe556bcd417f4ce9aaef267a786e31a2938902daaa0d36f69757"},
|
"ex_phone_number": {:hex, :ex_phone_number, "0.4.8", "c1c5e6f0673822a2a7b439b43af7d3eb1a5c19ae582b772b8b8d12625dd51ec1", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "43e2357c6b8cfe556bcd417f4ce9aaef267a786e31a2938902daaa0d36f69757"},
|
||||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||||
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
|
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
|
||||||
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
|
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
|
||||||
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
||||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
||||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
"igniter": {:hex, :igniter, "0.7.0", "6848714fa5afa14258c82924a57af9364745316241a409435cf39cbe11e3ae80", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "1e7254780dbf4b44c9eccd6d86d47aa961efc298d7f520c24acb0258c8e90ba9"},
|
"igniter": {:hex, :igniter, "0.7.2", "81c132c0df95963c7a228f74a32d3348773743ed9651f24183bfce0fe6ff16d1", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "f4cab73ec31f4fb452de1a17037f8a08826105265aa2d76486fcb848189bef9b"},
|
||||||
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
||||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||||
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
|
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
|
||||||
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
|
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
|
||||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||||
"live_debugger": {:hex, :live_debugger, "0.5.0", "95e0f7727d61010f7e9053923fb2a9416904a7533c2dfb36120e7684cba4c0af", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "73ebe95118d22aa402675f677abd731cb16b136d1b6ae5f4010441fb50753b14"},
|
"live_debugger": {:hex, :live_debugger, "0.5.1", "7302a4fda1920ba541b456c2d7a97acc3c7f9d7b938b5435927883b709c968a2", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "797fdca7cc60d7588c6e285b0d7ea73f2dce8b123bac43eae70271fa519bb907"},
|
||||||
"luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"},
|
"luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"},
|
||||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||||
|
|
@ -57,26 +57,26 @@
|
||||||
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
|
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
|
||||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
|
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
|
||||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"},
|
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.22", "9b3c985bfe38e82668594a8ce90008548f30b9f23b718ebaea4701710ce9006f", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e1395d5622d8bf02113cb58183589b3da6f1751af235768816e90cc3ec5f1188"},
|
||||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||||
"postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
|
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
||||||
"reactor": {:hex, :reactor, "0.17.0", "eb8bdb530dbae824e2d36a8538f8ec4f3aa7c2d1b61b04959fa787c634f88b49", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "3c3bf71693adbad9117b11ec83cfed7d5851b916ade508ed9718de7ae165bf25"},
|
"reactor": {:hex, :reactor, "1.0.0", "024bd13df910bcb8c01cebed4f10bd778269a141a1c8a234e4f67796ac4883cf", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "ae8eb507fffc517f5aa5947db9d2ede2db8bae63b66c94ccb5a2027d30f830a0"},
|
||||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||||
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
|
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
|
||||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||||
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
|
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
|
||||||
"sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"},
|
"sourceror": {:hex, :sourceror, "1.10.1", "325753ed460fe9fa34ebb4deda76d57b2e1507dcd78a5eb9e1c41bfb78b7cdfe", [:mix], [], "hexpm", "288f3079d93865cd1e3e20df5b884ef2cb440e0e03e8ae393624ee8a770ba588"},
|
||||||
"spark": {:hex, :spark, "2.3.14", "a08420d08e6e0e49d740aed3e160f1cb894ba8f6b3f5e6c63253e9df1995265c", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "af50c4ea5dd67eba822247f1c98e1d4e598cb7f6c28ccf5d002f0e0718096f4f"},
|
"spark": {:hex, :spark, "2.4.0", "f93d3ae6b5f3004e956d52f359fa40670366685447631bc7c058f4fbf250ebf3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "4e5185f5737cd987bb9ef377ae3462a55b8312f5007c2bc4ad6e850d14ac0111"},
|
||||||
"spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"},
|
"spitfire": {:hex, :spitfire, "0.3.1", "409b5ed3a2677df8790ed8b0542ca7e36c607d744fef4cb8cb8872fc80dd1803", [:mix], [], "hexpm", "72ff34d8f0096313a4b1a6505513c5ef4bbc0919bd8c181c07fc8d8dea8c9056"},
|
||||||
"splode": {:hex, :splode, "0.2.10", "f755ebc8e5dc1556869c0513cf5f3450be602a41e01196249306483c4badbec0", [:mix], [], "hexpm", "906b6dc17b7ebc9b9fd9a31360bf0bd691d20e934fb28795c0ddb0c19d3198f1"},
|
"splode": {:hex, :splode, "0.3.0", "ff8effecc509a51245df2f864ec78d849248647c37a75886033e3b1a53ca9470", [:mix], [], "hexpm", "73cfd0892d7316d6f2c93e6e8784bd6e137b2aa38443de52fd0a25171d106d81"},
|
||||||
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
||||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||||
"swoosh": {:hex, :swoosh, "1.20.0", "b04134c2b302da74c3a95ca4ddde191e4854d2847d6687783fecb023a9647598", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "13e610f709bae54851d68afb6862882aa646e5c974bf49e3bf5edd84a73cf213"},
|
"swoosh": {:hex, :swoosh, "1.21.0", "9f4fa629447774cfc9ad684d8a87a85384e8fce828b6390dd535dfbd43c9ee2a", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9127157bfb33b7e154d0f1ba4e888e14b08ede84e81dedcb318a2f33dbc6db51"},
|
||||||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||||
|
|
|
||||||
|
|
@ -2298,17 +2298,7 @@ msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV Datei. Da
|
||||||
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
||||||
msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld ist, erstellen Sie es in Mila vor dem Import."
|
msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld ist, erstellen Sie es in Mila vor dem Import."
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv/membership/member/validations/email_change_permission.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Custom Fields in CSV Import"
|
msgid "Only administrators or the linked user can change the email for members linked to users"
|
||||||
#~ msgstr "Benutzerdefinierte Felder"
|
msgstr "Nur Administrator*innen oder die verknüpfte Benutzer*in können die E-Mail von Mitgliedern ändern, die mit Benutzer*innen verknüpft sind."
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
|
||||||
#~ msgid "Individual data fields must be created in Mila before importing. Use the field name as the CSV column header. Unknown custom field columns will be ignored with a warning."
|
|
||||||
#~ msgstr "Individuelle Datenfelder müssen in Mila erstellt werden, bevor sie importiert werden können. Verwenden Sie den Namen des Datenfeldes als CSV-Spaltenüberschrift. Unbekannte Spaltenüberschriften werden mit einer Warnung ignoriert."
|
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
|
||||||
#~ #, elixir-autogen, elixir-format
|
|
||||||
#~ msgid "Manage Custom Fields"
|
|
||||||
#~ msgstr "Benutzerdefinierte Felder verwalten"
|
|
||||||
|
|
|
||||||
|
|
@ -2298,3 +2298,8 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv/membership/member/validations/email_change_permission.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Only administrators or the linked user can change the email for members linked to users"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -2299,17 +2299,7 @@ msgstr ""
|
||||||
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
||||||
msgstr "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
msgstr "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv/membership/member/validations/email_change_permission.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Custom Fields in CSV Import"
|
msgid "Only administrators or the linked user can change the email for members linked to users"
|
||||||
#~ msgstr ""
|
msgstr "Only administrators or the linked user can change the email for members linked to users"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
|
||||||
#~ msgid "Individual data fields must be created in Mila before importing. Use the field name as the CSV column header. Unknown custom field columns will be ignored with a warning."
|
|
||||||
#~ msgstr ""
|
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
|
||||||
#~ #, elixir-autogen, elixir-format
|
|
||||||
#~ msgid "Manage Custom Fields"
|
|
||||||
#~ msgstr ""
|
|
||||||
|
|
|
||||||
236
test/mv/membership/member_email_validation_test.exs
Normal file
236
test/mv/membership/member_email_validation_test.exs
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
defmodule Mv.Membership.MemberEmailValidationTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for Member email-change permission validation.
|
||||||
|
|
||||||
|
When a member is linked to a user, only admins or the linked user may change
|
||||||
|
that member's email. Unlinked members and non-email updates are unaffected.
|
||||||
|
"""
|
||||||
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
|
alias Mv.Accounts
|
||||||
|
alias Mv.Authorization
|
||||||
|
alias Mv.Helpers.SystemActor
|
||||||
|
alias Mv.Membership
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = SystemActor.get_system_actor()
|
||||||
|
%{actor: system_actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_role_with_permission_set(permission_set_name, actor) do
|
||||||
|
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||||
|
|
||||||
|
case Authorization.create_role(
|
||||||
|
%{
|
||||||
|
name: role_name,
|
||||||
|
description: "Test role for #{permission_set_name}",
|
||||||
|
permission_set_name: permission_set_name
|
||||||
|
},
|
||||||
|
actor: actor
|
||||||
|
) do
|
||||||
|
{:ok, role} -> role
|
||||||
|
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_user_with_permission_set(permission_set_name, actor) do
|
||||||
|
role = create_role_with_permission_set(permission_set_name, actor)
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
Accounts.User
|
||||||
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||||
|
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||||
|
password: "testpassword123"
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||||
|
|> Ash.update(actor: actor)
|
||||||
|
|
||||||
|
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
||||||
|
user_with_role
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_admin_user(actor) do
|
||||||
|
create_user_with_permission_set("admin", actor)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_linked_member_for_user(user, actor) do
|
||||||
|
admin = create_admin_user(actor)
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(
|
||||||
|
%{
|
||||||
|
first_name: "Linked",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "linked#{System.unique_integer([:positive])}@example.com"
|
||||||
|
},
|
||||||
|
actor: admin
|
||||||
|
)
|
||||||
|
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
|
||||||
|
|> Ash.update(actor: admin, domain: Mv.Accounts, return_notifications?: false)
|
||||||
|
|
||||||
|
member
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_unlinked_member(actor) do
|
||||||
|
admin = create_admin_user(actor)
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(
|
||||||
|
%{
|
||||||
|
first_name: "Unlinked",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
||||||
|
},
|
||||||
|
actor: admin
|
||||||
|
)
|
||||||
|
|
||||||
|
member
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unlinked member" do
|
||||||
|
test "normal_user can update email of unlinked member", %{actor: actor} do
|
||||||
|
normal_user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
unlinked_member = create_unlinked_member(actor)
|
||||||
|
|
||||||
|
new_email = "new#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Membership.update_member(unlinked_member, %{email: new_email}, actor: normal_user)
|
||||||
|
|
||||||
|
assert updated.email == new_email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validation does not block when member has no linked user", %{actor: actor} do
|
||||||
|
normal_user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
unlinked_member = create_unlinked_member(actor)
|
||||||
|
|
||||||
|
new_email = "other#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:ok, _} =
|
||||||
|
Membership.update_member(unlinked_member, %{email: new_email}, actor: normal_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "linked member – another user's member" do
|
||||||
|
test "normal_user cannot update email of another user's linked member", %{actor: actor} do
|
||||||
|
user_a = create_user_with_permission_set("own_data", actor)
|
||||||
|
linked_member = create_linked_member_for_user(user_a, actor)
|
||||||
|
|
||||||
|
normal_user_b = create_user_with_permission_set("normal_user", actor)
|
||||||
|
new_email = "other#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||||
|
Membership.update_member(linked_member, %{email: new_email}, actor: normal_user_b)
|
||||||
|
|
||||||
|
assert Enum.any?(error.errors, &(&1.field == :email)),
|
||||||
|
"expected an error for field :email, got: #{inspect(error.errors)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can update email of linked member", %{actor: actor} do
|
||||||
|
user_a = create_user_with_permission_set("own_data", actor)
|
||||||
|
linked_member = create_linked_member_for_user(user_a, actor)
|
||||||
|
admin = create_admin_user(actor)
|
||||||
|
|
||||||
|
new_email = "admin_changed#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Membership.update_member(linked_member, %{email: new_email}, actor: admin)
|
||||||
|
|
||||||
|
assert updated.email == new_email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "linked member – own member" do
|
||||||
|
test "own_data user can update email of their own linked member", %{actor: actor} do
|
||||||
|
own_data_user = create_user_with_permission_set("own_data", actor)
|
||||||
|
linked_member = create_linked_member_for_user(own_data_user, actor)
|
||||||
|
|
||||||
|
{:ok, own_data_user} =
|
||||||
|
Ash.get(Accounts.User, own_data_user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||||
|
|
||||||
|
{:ok, own_data_user} =
|
||||||
|
Ash.load(own_data_user, :member, domain: Mv.Accounts, actor: actor)
|
||||||
|
|
||||||
|
new_email = "own_updated#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Membership.update_member(linked_member, %{email: new_email}, actor: own_data_user)
|
||||||
|
|
||||||
|
assert updated.email == new_email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "normal_user with linked member can update email of that same member", %{actor: actor} do
|
||||||
|
normal_user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
linked_member = create_linked_member_for_user(normal_user, actor)
|
||||||
|
|
||||||
|
{:ok, normal_user} =
|
||||||
|
Ash.get(Accounts.User, normal_user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||||
|
|
||||||
|
{:ok, normal_user} = Ash.load(normal_user, :member, domain: Mv.Accounts, actor: actor)
|
||||||
|
|
||||||
|
new_email = "normal_own#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Membership.update_member(linked_member, %{email: new_email}, actor: normal_user)
|
||||||
|
|
||||||
|
assert updated.email == new_email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "no-op / other fields" do
|
||||||
|
test "updating only other attributes on linked member as normal_user does not trigger validation error",
|
||||||
|
%{actor: actor} do
|
||||||
|
user_a = create_user_with_permission_set("own_data", actor)
|
||||||
|
linked_member = create_linked_member_for_user(user_a, actor)
|
||||||
|
normal_user_b = create_user_with_permission_set("normal_user", actor)
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Membership.update_member(linked_member, %{first_name: "UpdatedName"},
|
||||||
|
actor: normal_user_b
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated.first_name == "UpdatedName"
|
||||||
|
assert updated.email == linked_member.email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating email of linked member as admin succeeds", %{actor: actor} do
|
||||||
|
user_a = create_user_with_permission_set("own_data", actor)
|
||||||
|
linked_member = create_linked_member_for_user(user_a, actor)
|
||||||
|
admin = create_admin_user(actor)
|
||||||
|
|
||||||
|
new_email = "admin_ok#{System.unique_integer([:positive])}@example.com"
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
Membership.update_member(linked_member, %{email: new_email}, actor: admin)
|
||||||
|
|
||||||
|
assert updated.email == new_email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "read_only" do
|
||||||
|
test "read_only cannot update any member (policy rejects before validation)", %{actor: actor} do
|
||||||
|
read_only_user = create_user_with_permission_set("read_only", actor)
|
||||||
|
linked_member = create_linked_member_for_user(read_only_user, actor)
|
||||||
|
|
||||||
|
{:ok, read_only_user} =
|
||||||
|
Ash.get(Accounts.User, read_only_user.id,
|
||||||
|
domain: Mv.Accounts,
|
||||||
|
load: [:role],
|
||||||
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
Membership.update_member(linked_member, %{email: "changed@example.com"},
|
||||||
|
actor: read_only_user
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -22,9 +22,14 @@ defmodule MvWeb.Layouts.SidebarTest do
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Returns assigns for an authenticated user with all required attributes.
|
# Returns assigns for an authenticated user with all required attributes.
|
||||||
|
# User has admin role so can_access_page? returns true for all sidebar links.
|
||||||
defp authenticated_assigns(mobile \\ false) do
|
defp authenticated_assigns(mobile \\ false) do
|
||||||
%{
|
%{
|
||||||
current_user: %{id: "user-123", email: "test@example.com"},
|
current_user: %{
|
||||||
|
id: "user-123",
|
||||||
|
email: "test@example.com",
|
||||||
|
role: %{permission_set_name: "admin"}
|
||||||
|
},
|
||||||
club_name: "Test Club",
|
club_name: "Test Club",
|
||||||
mobile: mobile
|
mobile: mobile
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +149,9 @@ defmodule MvWeb.Layouts.SidebarTest do
|
||||||
assert menu_item_count > 0, "Should have at least one top-level menu item"
|
assert menu_item_count > 0, "Should have at least one top-level menu item"
|
||||||
|
|
||||||
# Check that nested menu groups exist
|
# Check that nested menu groups exist
|
||||||
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
|
assert html =~
|
||||||
|
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
|
||||||
|
|
||||||
assert html =~ ~s(role="group")
|
assert html =~ ~s(role="group")
|
||||||
assert has_class?(html, "expanded-menu-group")
|
assert has_class?(html, "expanded-menu-group")
|
||||||
|
|
||||||
|
|
@ -193,7 +200,9 @@ defmodule MvWeb.Layouts.SidebarTest do
|
||||||
html = render_sidebar(authenticated_assigns())
|
html = render_sidebar(authenticated_assigns())
|
||||||
|
|
||||||
# Check for nested menu structure
|
# Check for nested menu structure
|
||||||
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
|
assert html =~
|
||||||
|
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
|
||||||
|
|
||||||
assert html =~ ~s(role="group")
|
assert html =~ ~s(role="group")
|
||||||
assert html =~ ~s(aria-label="Administration")
|
assert html =~ ~s(aria-label="Administration")
|
||||||
assert has_class?(html, "expanded-menu-group")
|
assert has_class?(html, "expanded-menu-group")
|
||||||
|
|
@ -521,7 +530,9 @@ defmodule MvWeb.Layouts.SidebarTest do
|
||||||
assert html =~ ~s(role="menuitem")
|
assert html =~ ~s(role="menuitem")
|
||||||
|
|
||||||
# Check that nested menus exist
|
# Check that nested menus exist
|
||||||
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
|
assert html =~
|
||||||
|
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
|
||||||
|
|
||||||
assert html =~ ~s(role="group")
|
assert html =~ ~s(role="group")
|
||||||
|
|
||||||
# Footer section
|
# Footer section
|
||||||
|
|
@ -629,7 +640,9 @@ defmodule MvWeb.Layouts.SidebarTest do
|
||||||
html = render_sidebar(authenticated_assigns())
|
html = render_sidebar(authenticated_assigns())
|
||||||
|
|
||||||
# expanded-menu-group structure present
|
# expanded-menu-group structure present
|
||||||
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
|
assert html =~
|
||||||
|
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
|
||||||
|
|
||||||
assert html =~ ~s(role="group")
|
assert html =~ ~s(role="group")
|
||||||
assert html =~ ~s(aria-label="Administration")
|
assert html =~ ~s(aria-label="Administration")
|
||||||
assert has_class?(html, "expanded-menu-group")
|
assert has_class?(html, "expanded-menu-group")
|
||||||
|
|
@ -843,7 +856,9 @@ defmodule MvWeb.Layouts.SidebarTest do
|
||||||
|
|
||||||
# Expanded menu group should have correct structure
|
# Expanded menu group should have correct structure
|
||||||
# (CSS handles hover effects, but we verify structure)
|
# (CSS handles hover effects, but we verify structure)
|
||||||
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
|
assert html =~
|
||||||
|
~s(<li role="none" class="expanded-menu-group" data-testid="sidebar-administration">)
|
||||||
|
|
||||||
assert html =~ ~s(role="group")
|
assert html =~ ~s(role="group")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
120
test/mv_web/components/sidebar_authorization_test.exs
Normal file
120
test/mv_web/components/sidebar_authorization_test.exs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
defmodule MvWeb.SidebarAuthorizationTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for sidebar menu visibility based on user permissions (can_access_page?).
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import MvWeb.Layouts.Sidebar
|
||||||
|
|
||||||
|
alias Mv.Fixtures
|
||||||
|
|
||||||
|
defp render_sidebar(assigns) do
|
||||||
|
render_component(&sidebar/1, assigns)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sidebar_assigns(current_user, opts \\ []) do
|
||||||
|
mobile = Keyword.get(opts, :mobile, false)
|
||||||
|
club_name = Keyword.get(opts, :club_name, "Test Club")
|
||||||
|
|
||||||
|
%{
|
||||||
|
current_user: current_user,
|
||||||
|
club_name: club_name,
|
||||||
|
mobile: mobile
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "sidebar menu with admin user" do
|
||||||
|
test "shows Members, Fee Types and Administration with all subitems" do
|
||||||
|
user = Fixtures.user_with_role_fixture("admin")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
assert html =~ ~s(href="/members")
|
||||||
|
assert html =~ ~s(href="/membership_fee_types")
|
||||||
|
assert html =~ ~s(data-testid="sidebar-administration")
|
||||||
|
assert html =~ ~s(href="/users")
|
||||||
|
assert html =~ ~s(href="/groups")
|
||||||
|
assert html =~ ~s(href="/admin/roles")
|
||||||
|
assert html =~ ~s(href="/membership_fee_settings")
|
||||||
|
assert html =~ ~s(href="/settings")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "sidebar menu with read_only user (Vorstand/Buchhaltung)" do
|
||||||
|
test "shows Members and Groups (from Administration)" do
|
||||||
|
user = Fixtures.user_with_role_fixture("read_only")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
assert html =~ ~s(href="/members")
|
||||||
|
assert html =~ ~s(href="/groups")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not show Fee Types, Users, Roles or Settings" do
|
||||||
|
user = Fixtures.user_with_role_fixture("read_only")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
refute html =~ ~s(href="/membership_fee_types")
|
||||||
|
refute html =~ ~s(href="/users")
|
||||||
|
refute html =~ ~s(href="/admin/roles")
|
||||||
|
refute html =~ ~s(href="/settings")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "sidebar menu with normal_user (Kassenwart)" do
|
||||||
|
test "shows Members and Groups" do
|
||||||
|
user = Fixtures.user_with_role_fixture("normal_user")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
assert html =~ ~s(href="/members")
|
||||||
|
assert html =~ ~s(href="/groups")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not show Fee Types, Users, Roles or Settings" do
|
||||||
|
user = Fixtures.user_with_role_fixture("normal_user")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
refute html =~ ~s(href="/membership_fee_types")
|
||||||
|
refute html =~ ~s(href="/users")
|
||||||
|
refute html =~ ~s(href="/admin/roles")
|
||||||
|
refute html =~ ~s(href="/settings")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "sidebar menu with own_data user (Mitglied)" do
|
||||||
|
test "does not show Members link (no /members page access)" do
|
||||||
|
user = Fixtures.user_with_role_fixture("own_data")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
refute html =~ ~s(href="/members")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not show Fee Types or Administration" do
|
||||||
|
user = Fixtures.user_with_role_fixture("own_data")
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
refute html =~ ~s(href="/membership_fee_types")
|
||||||
|
refute html =~ ~s(href="/users")
|
||||||
|
refute html =~ ~s(data-testid="sidebar-administration")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "sidebar with nil current_user" do
|
||||||
|
test "does not render menu items (only header and footer when present)" do
|
||||||
|
html = render_sidebar(sidebar_assigns(nil))
|
||||||
|
|
||||||
|
refute html =~ ~s(role="menubar")
|
||||||
|
refute html =~ ~s(href="/members")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "sidebar with user without role" do
|
||||||
|
test "does not show any navigation links" do
|
||||||
|
user = %{id: "user-no-role", email: "noreply@test.com", role: nil}
|
||||||
|
html = render_sidebar(sidebar_assigns(user))
|
||||||
|
|
||||||
|
refute html =~ ~s(href="/members")
|
||||||
|
refute html =~ ~s(href="/membership_fee_types")
|
||||||
|
refute html =~ ~s(href="/users")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
102
test/mv_web/live/member_live_authorization_test.exs
Normal file
102
test/mv_web/live/member_live_authorization_test.exs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
defmodule MvWeb.MemberLiveAuthorizationTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for UI authorization on Member LiveViews (Index and Show).
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias Mv.Fixtures
|
||||||
|
|
||||||
|
describe "Member Index - Vorstand (read_only)" do
|
||||||
|
@tag role: :read_only
|
||||||
|
test "sees member list but not New Member button", %{conn: conn} do
|
||||||
|
_member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
refute has_element?(view, "[data-testid=member-new]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :read_only
|
||||||
|
test "does not see Edit or Delete buttons in table", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
refute has_element?(view, "#row-#{member.id} [data-testid=member-edit]")
|
||||||
|
refute has_element?(view, "#row-#{member.id} [data-testid=member-delete]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Member Index - Kassenwart (normal_user)" do
|
||||||
|
@tag role: :normal_user
|
||||||
|
test "sees New Member and Edit buttons", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=member-new]")
|
||||||
|
assert has_element?(view, "#row-#{member.id} [data-testid=member-edit]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :normal_user
|
||||||
|
test "does not see Delete button", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
refute has_element?(view, "#row-#{member.id} [data-testid=member-delete]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Member Index - Admin" do
|
||||||
|
@tag role: :admin
|
||||||
|
test "sees New Member, Edit and Delete buttons", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=member-new]")
|
||||||
|
assert has_element?(view, "#row-#{member.id} [data-testid=member-edit]")
|
||||||
|
assert has_element?(view, "#row-#{member.id} [data-testid=member-delete]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Member Index - Mitglied (own_data)" do
|
||||||
|
@tag role: :member
|
||||||
|
test "is redirected when accessing /members", %{conn: conn, current_user: user} do
|
||||||
|
assert {:error, {:redirect, %{to: to}}} = live(conn, "/members")
|
||||||
|
assert to == "/users/#{user.id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Member Show - Edit button visibility" do
|
||||||
|
@tag role: :admin
|
||||||
|
test "admin sees Edit button", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=member-edit]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :read_only
|
||||||
|
test "read_only does not see Edit button", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||||
|
|
||||||
|
refute has_element?(view, "[data-testid=member-edit]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :normal_user
|
||||||
|
test "normal_user sees Edit button", %{conn: conn} do
|
||||||
|
member = Fixtures.member_fixture()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/members/#{member.id}")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=member-edit]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
81
test/mv_web/live/user_live_authorization_test.exs
Normal file
81
test/mv_web/live/user_live_authorization_test.exs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
defmodule MvWeb.UserLiveAuthorizationTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for UI authorization on User LiveViews (Index and Show).
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias Mv.Fixtures
|
||||||
|
|
||||||
|
describe "User Index - Admin" do
|
||||||
|
@tag role: :admin
|
||||||
|
test "sees New User, Edit and Delete buttons", %{conn: conn} do
|
||||||
|
user = Fixtures.user_with_role_fixture("admin")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/users")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=user-new]")
|
||||||
|
assert has_element?(view, "#row-#{user.id} [data-testid=user-edit]")
|
||||||
|
assert has_element?(view, "#row-#{user.id} [data-testid=user-delete]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "User Index - Non-Admin is redirected" do
|
||||||
|
@tag role: :read_only
|
||||||
|
test "read_only is redirected when accessing /users", %{conn: conn, current_user: user} do
|
||||||
|
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users")
|
||||||
|
assert to == "/users/#{user.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :member
|
||||||
|
test "member is redirected when accessing /users", %{conn: conn, current_user: user} do
|
||||||
|
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users")
|
||||||
|
assert to == "/users/#{user.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :normal_user
|
||||||
|
test "normal_user is redirected when accessing /users", %{conn: conn, current_user: user} do
|
||||||
|
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users")
|
||||||
|
assert to == "/users/#{user.id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "User Show - own profile" do
|
||||||
|
@tag role: :member
|
||||||
|
test "member sees Edit button on own profile", %{conn: conn, current_user: user} do
|
||||||
|
{:ok, view, _html} = live(conn, "/users/#{user.id}")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=user-edit]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :read_only
|
||||||
|
test "read_only sees Edit button on own profile", %{conn: conn, current_user: user} do
|
||||||
|
{:ok, view, _html} = live(conn, "/users/#{user.id}")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=user-edit]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag role: :admin
|
||||||
|
test "admin sees Edit button on user show", %{conn: conn} do
|
||||||
|
user = Fixtures.user_with_role_fixture("read_only")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/users/#{user.id}")
|
||||||
|
|
||||||
|
assert has_element?(view, "[data-testid=user-edit]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "User Show - other user (non-admin redirected)" do
|
||||||
|
@tag role: :member
|
||||||
|
test "member is redirected when accessing other user's profile", %{
|
||||||
|
conn: conn,
|
||||||
|
current_user: current_user
|
||||||
|
} do
|
||||||
|
other_user = Fixtures.user_with_role_fixture("admin")
|
||||||
|
|
||||||
|
assert {:error, {:redirect, %{to: to}}} = live(conn, "/users/#{other_user.id}")
|
||||||
|
assert to == "/users/#{current_user.id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue