241 lines
7.5 KiB
Elixir
241 lines
7.5 KiB
Elixir
defmodule MvWeb.RoleLive.Form do
|
|
@moduledoc """
|
|
LiveView form for creating and editing roles.
|
|
|
|
## Features
|
|
- Create new roles
|
|
- Edit existing roles (name, description, permission_set_name)
|
|
- Custom dropdown for permission_set_name with badges
|
|
- Form validation
|
|
|
|
## Security
|
|
Only admins can access this page (enforced by authorization).
|
|
"""
|
|
use MvWeb, :live_view
|
|
|
|
alias Mv.Authorization.PermissionSets
|
|
|
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>{gettext("Use this form to manage roles in your database.")}</:subtitle>
|
|
</.header>
|
|
|
|
<.form class="max-w-xl" for={@form} id="role-form" phx-change="validate" phx-submit="save">
|
|
<.input field={@form[:name]} type="text" label={gettext("Name")} required />
|
|
|
|
<.input
|
|
field={@form[:description]}
|
|
type="textarea"
|
|
label={gettext("Description")}
|
|
rows="3"
|
|
/>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="role-form_permission_set_name">
|
|
<span class="label-text font-semibold">
|
|
{gettext("Permission Set")}
|
|
<span class="text-red-700">*</span>
|
|
</span>
|
|
</label>
|
|
<select
|
|
class={[
|
|
"select select-bordered w-full",
|
|
@form.errors[:permission_set_name] && "select-error"
|
|
]}
|
|
name="role[permission_set_name]"
|
|
id="role-form_permission_set_name"
|
|
required
|
|
aria-label={gettext("Permission Set")}
|
|
>
|
|
<option value="">{gettext("Select permission set")}</option>
|
|
<%= for permission_set <- all_permission_sets() do %>
|
|
<option
|
|
value={permission_set}
|
|
selected={@form[:permission_set_name].value == permission_set}
|
|
>
|
|
{format_permission_set_option(permission_set)}
|
|
</option>
|
|
<% end %>
|
|
</select>
|
|
<%= if @form.errors[:permission_set_name] do %>
|
|
<%= for error <- List.wrap(@form.errors[:permission_set_name]) do %>
|
|
<% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %>
|
|
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
|
|
<.icon name="hero-exclamation-circle" class="size-5" />
|
|
{msg}
|
|
</p>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
|
{gettext("Save Role")}
|
|
</.button>
|
|
<.button navigate={return_path(@return_to, @role)} type="button">
|
|
{gettext("Cancel")}
|
|
</.button>
|
|
</div>
|
|
</.form>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
|
|
@impl true
|
|
def mount(params, _session, socket) do
|
|
case params["id"] do
|
|
nil ->
|
|
action = gettext("New")
|
|
page_title = action <> " " <> gettext("Role")
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:return_to, return_to(params["return_to"]))
|
|
|> assign(:role, nil)
|
|
|> assign(:page_title, page_title)
|
|
|> assign_form()}
|
|
|
|
id ->
|
|
try do
|
|
case Ash.get(
|
|
Mv.Authorization.Role,
|
|
id,
|
|
domain: Mv.Authorization,
|
|
actor: socket.assigns[:current_user]
|
|
) do
|
|
{:ok, role} ->
|
|
action = gettext("Edit")
|
|
page_title = action <> " " <> gettext("Role")
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:return_to, return_to(params["return_to"]))
|
|
|> assign(:role, role)
|
|
|> assign(:page_title, page_title)
|
|
|> assign_form()}
|
|
|
|
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} ->
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, gettext("Role not found."))
|
|
|> redirect(to: ~p"/admin/roles")}
|
|
|
|
{:error, error} ->
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, format_error(error))
|
|
|> redirect(to: ~p"/admin/roles")}
|
|
end
|
|
rescue
|
|
e in [Ash.Error.Invalid] ->
|
|
case e do
|
|
%Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]} ->
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, gettext("Role not found."))
|
|
|> redirect(to: ~p"/admin/roles")}
|
|
|
|
_ ->
|
|
reraise e, __STACKTRACE__
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec return_to(String.t() | nil) :: String.t()
|
|
defp return_to("show"), do: "show"
|
|
defp return_to(_), do: "index"
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"role" => role_params}, socket) do
|
|
validated_form = AshPhoenix.Form.validate(socket.assigns.form, role_params)
|
|
{:noreply, assign(socket, form: validated_form)}
|
|
end
|
|
|
|
def handle_event("save", %{"role" => role_params}, socket) do
|
|
case AshPhoenix.Form.submit(socket.assigns.form, params: role_params) do
|
|
{:ok, role} ->
|
|
notify_parent({:saved, role})
|
|
|
|
redirect_path =
|
|
if socket.assigns.return_to == "show" do
|
|
~p"/admin/roles/#{role.id}"
|
|
else
|
|
~p"/admin/roles"
|
|
end
|
|
|
|
socket =
|
|
socket
|
|
|> put_flash(:info, gettext("Role saved successfully."))
|
|
|> push_navigate(to: redirect_path)
|
|
|
|
{:noreply, socket}
|
|
|
|
{:error, form} ->
|
|
{:noreply, assign(socket, form: form)}
|
|
end
|
|
end
|
|
|
|
@spec notify_parent(any()) :: any()
|
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
|
|
|
@spec assign_form(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
|
|
defp assign_form(%{assigns: %{role: role, current_user: actor}} = socket) do
|
|
form =
|
|
if role do
|
|
AshPhoenix.Form.for_update(role, :update_role,
|
|
domain: Mv.Authorization,
|
|
as: "role",
|
|
actor: actor
|
|
)
|
|
else
|
|
AshPhoenix.Form.for_create(
|
|
Mv.Authorization.Role,
|
|
:create_role,
|
|
domain: Mv.Authorization,
|
|
as: "role",
|
|
actor: actor
|
|
)
|
|
end
|
|
|
|
assign(socket, form: to_form(form))
|
|
end
|
|
|
|
defp all_permission_sets do
|
|
PermissionSets.all_permission_sets() |> Enum.map(&Atom.to_string/1)
|
|
end
|
|
|
|
defp format_permission_set_option("own_data"),
|
|
do: gettext("own_data - Access only to own data")
|
|
|
|
defp format_permission_set_option("read_only"),
|
|
do: gettext("read_only - Read access to all data")
|
|
|
|
defp format_permission_set_option("normal_user"),
|
|
do: gettext("normal_user - Create/Read/Update access")
|
|
|
|
defp format_permission_set_option("admin"),
|
|
do: gettext("admin - Unrestricted access")
|
|
|
|
defp format_permission_set_option(set), do: set
|
|
|
|
@spec return_path(String.t(), Mv.Authorization.Role.t() | nil) :: String.t()
|
|
defp return_path("index", _role), do: ~p"/admin/roles"
|
|
defp return_path("show", role) when not is_nil(role), do: ~p"/admin/roles/#{role.id}"
|
|
defp return_path("show", _role), do: ~p"/admin/roles"
|
|
defp return_path(_, role) when not is_nil(role), do: ~p"/admin/roles/#{role.id}"
|
|
defp return_path(_, _role), do: ~p"/admin/roles"
|
|
|
|
defp format_error(%Ash.Error.Invalid{} = error) do
|
|
Enum.map_join(error.errors, ", ", fn e -> e.message end)
|
|
end
|
|
|
|
defp format_error(error) when is_binary(error), do: error
|
|
defp format_error(_error), do: gettext("An error occurred")
|
|
end
|