diff --git a/lib/safira/accounts.ex b/lib/safira/accounts.ex index 19644a938..32cdce1c4 100644 --- a/lib/safira/accounts.ex +++ b/lib/safira/accounts.ex @@ -182,15 +182,16 @@ defmodule Safira.Accounts do |> Flop.validate_and_run(params, for: User) end - def update_staff(%Staff{} = staff, attrs) do - Staff.changeset(staff, attrs) + def update_user(%User{} = user, attrs) do + user + |> User.changeset(attrs) |> Repo.update() end @doc """ Changes a staff """ - def change_staff(%Staff{} = staff, attrs) do + def change_staff(%Staff{} = staff, attrs \\ %{}) do Staff.changeset(staff, attrs) end @@ -289,9 +290,34 @@ defmodule Safira.Accounts do """ def register_staff_user(attrs) do - %User{} - |> User.registration_changeset(attrs |> Map.put(:type, :staff)) - |> Repo.insert() + attrs = attrs |> Map.put("type", :staff) + + Ecto.Multi.new() + |> Ecto.Multi.insert( + :user, + User.registration_changeset( + %User{}, + Map.delete(attrs, "staff"), + hash_password: true, + validate_email: true + ) + ) + |> Ecto.Multi.insert( + :staff, + fn %{user: user} -> + staff_attrs = + attrs + |> Map.get("staff") + |> Map.put("user_id", user.id) + + Staff.changeset(%Staff{}, staff_attrs) + end + ) + |> Ecto.Multi.update( + :new_user, + fn %{user: user} -> User.confirm_changeset(user) end + ) + |> Repo.transaction() end @doc """ diff --git a/lib/safira/accounts/staff.ex b/lib/safira/accounts/staff.ex index ed603b4a2..be2850e32 100644 --- a/lib/safira/accounts/staff.ex +++ b/lib/safira/accounts/staff.ex @@ -4,11 +4,13 @@ defmodule Safira.Accounts.Staff do """ use Safira.Schema + alias Safira.Accounts.User + @required_fields ~w(user_id role_id)a @optional_fields ~w()a schema "staffs" do - belongs_to :user, Safira.Accounts.User + belongs_to :user, User belongs_to :role, Safira.Accounts.Role timestamps(type: :utc_datetime) @@ -17,7 +19,6 @@ defmodule Safira.Accounts.Staff do def changeset(staff, attrs) do staff |> cast(attrs, @required_fields ++ @optional_fields) - |> cast_assoc(:user) |> validate_required(@required_fields) end end diff --git a/lib/safira/accounts/user.ex b/lib/safira/accounts/user.ex index 253754beb..3b03ea3b5 100644 --- a/lib/safira/accounts/user.ex +++ b/lib/safira/accounts/user.ex @@ -44,7 +44,7 @@ defmodule Safira.Accounts.User do field :allows_marketing, :boolean, default: false has_one :attendee, Attendee, on_delete: :delete_all - has_one :staff, Staff, on_delete: :delete_all + has_one :staff, Staff, on_delete: :delete_all, on_replace: :update has_one :company, Company, on_delete: :delete_all timestamps(type: :utc_datetime) @@ -81,6 +81,18 @@ defmodule Safira.Accounts.User do |> validate_handle() |> validate_password(opts) |> cast_assoc(:attendee, with: &Attendee.changeset/2) + |> cast_assoc(:staff, with: &Staff.changeset/2) + end + + def changeset(user, attrs, opts \\ []) do + user + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields |> Enum.reject(&(&1 in [:email, :password, :handle]))) + |> validate_email(opts) + |> validate_handle() + |> validate_length(:password, min: 12, max: 72) + |> cast_assoc(:attendee, with: &Attendee.changeset/2) + |> cast_assoc(:staff, with: &Staff.changeset/2) end @doc """ diff --git a/lib/safira/release.ex b/lib/safira/release.ex index a9c1a0bc7..688bf1465 100644 --- a/lib/safira/release.ex +++ b/lib/safira/release.ex @@ -41,19 +41,13 @@ defmodule Safira.Release do end # Create user - case Safira.Accounts.register_staff_user(%{ - name: name, - email: email, - password: password, - handle: handle - }) do - {:ok, user} -> - # Assign role to user - Safira.Accounts.create_staff(%{user_id: user.id, role_id: role.id}) - - {:error, changeset} -> - {:error, changeset} - end + Safira.Accounts.register_staff_user(%{ + name: name, + email: email, + password: password, + handle: handle, + role_id: role.id + }) end defp repos do diff --git a/lib/safira_web/live/backoffice/staff_live/form_component.ex b/lib/safira_web/live/backoffice/staff_live/form_component.ex index d806ce51b..627760a27 100644 --- a/lib/safira_web/live/backoffice/staff_live/form_component.ex +++ b/lib/safira_web/live/backoffice/staff_live/form_component.ex @@ -3,7 +3,8 @@ defmodule SafiraWeb.Backoffice.StaffLive.FormComponent do import SafiraWeb.Components.Forms alias Safira.Accounts - alias Safira.Roles + alias Safira.Accounts.User + alias Safira.Repo @impl true def render(assigns) do @@ -19,7 +20,18 @@ defmodule SafiraWeb.Backoffice.StaffLive.FormComponent do >
- <.field field={@form[:role_id]} type="select" label="Role" options={@roles} required /> + <.field field={@form[:name]} type="text" label="Name" required /> + <.field field={@form[:email]} type="email" label="Email" required /> + <.field + field={@form[:password]} + type="password" + label="Password" + required={@action == :new} + /> + <.field field={@form[:handle]} type="text" label="Handle" required /> + <.inputs_for :let={u} field={@form[:staff]}> + <.field field={u[:role_id]} type="select" label="Role" options={@roles} required /> +
<:actions> @@ -33,41 +45,51 @@ defmodule SafiraWeb.Backoffice.StaffLive.FormComponent do @impl true def mount(socket) do - roles = - Roles.list_roles() - |> Enum.map(&{&1.name, &1.id}) - - {:ok, - socket - |> assign(:roles, roles)} + {:ok, socket} end @impl true - def update(%{id: id} = assigns, socket) do - staff = Accounts.get_staff!(id) - change_staff = Accounts.change_staff(staff, %{}) - form = to_form(change_staff, as: "staff") + def update(%{user: user} = assigns, socket) do + user = user |> Repo.preload(:staff) + user_changeset = Accounts.change_user_registration(user) {:ok, socket |> assign(assigns) - |> assign(:staff, staff) - |> assign(:form, form)} + |> assign(:user, user) + |> assign(:form, to_form(user_changeset))} end @impl true - def handle_event("validate", %{"staff" => staff_params}, socket) do - {:noreply, - socket - |> assign_new(:form, fn -> - to_form(Accounts.change_staff(socket.assigns.staff, staff_params)) - end)} + def handle_event("validate", %{"user" => user_params}, socket) do + changeset = + Accounts.change_user_registration(%User{}, user_params) + + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} end - def handle_event("save", %{"staff" => staff_params}, socket) do - staff = socket.assigns.staff + def handle_event("save", %{"user" => user_params}, socket) do + save_staff(socket, socket.assigns.action, user_params) + end - case Accounts.update_staff(staff, staff_params) do + defp save_staff(socket, :new, user_params) do + case Accounts.register_staff_user(user_params) do + {:ok, _staff} -> + {:noreply, + socket + |> put_flash(:info, "Staff created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, _, %Ecto.Changeset{} = changeset, _} -> + {:noreply, + socket + |> put_flash(:error, gettext("Something went wrong while creating the staff.")) + |> assign(:form, to_form(changeset, action: :validate))} + end + end + + defp save_staff(socket, :edit, user_params) do + case Accounts.update_user(socket.assigns.user, user_params) do {:ok, _staff} -> {:noreply, socket @@ -78,8 +100,7 @@ defmodule SafiraWeb.Backoffice.StaffLive.FormComponent do {:noreply, socket |> put_flash(:error, gettext("Something went wrong when updating the staff.")) - |> assign(:form, to_form(changeset)) - |> push_patch(to: socket.assigns.patch)} + |> assign(:form, to_form(changeset))} end end end diff --git a/lib/safira_web/live/backoffice/staff_live/index.ex b/lib/safira_web/live/backoffice/staff_live/index.ex index 7dfd44769..618096889 100644 --- a/lib/safira_web/live/backoffice/staff_live/index.ex +++ b/lib/safira_web/live/backoffice/staff_live/index.ex @@ -7,8 +7,13 @@ defmodule SafiraWeb.Backoffice.StaffLive.Index do import SafiraWeb.Components.{Table, TableSearch} + alias Safira.Accounts.User + alias Safira.Repo + alias Safira.Roles + on_mount {SafiraWeb.StaffRoles, index: %{"staffs" => ["show"]}, + new: %{"staffs" => ["edit"]}, edit: %{"staffs" => ["edit"]}, roles: %{"staffs" => ["roles_edit"]}, roles_edit: %{"staffs" => ["roles_edit"]}, @@ -32,7 +37,8 @@ defmodule SafiraWeb.Backoffice.StaffLive.Index do |> assign(:meta, meta) |> assign(:params, params) |> assign(:current_page, :staffs) - |> assign(:staffs, staffs)} + |> assign(:staffs, staffs) + |> apply_action(socket.assigns.live_action, params)} {:error, _} -> {:noreply, socket} @@ -67,4 +73,38 @@ defmodule SafiraWeb.Backoffice.StaffLive.Index do _ -> Map.put(staff, :is_online, value) end end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Staffs") + end + + defp apply_action(socket, :new, _params) do + roles = + Roles.list_roles() + |> Enum.map(&{&1.name, &1.id}) + + socket + |> assign(:page_title, "New Staff") + |> assign(:user, %User{}) + |> assign(:roles, roles) + end + + defp apply_action(socket, :edit, %{"id" => id}) do + roles = + Roles.list_roles() + |> Enum.map(&{&1.name, &1.id}) + + staff = Accounts.get_staff!(id) |> Repo.preload(:user) + user = if staff.user, do: staff.user, else: %User{} + + socket + |> assign(:page_title, "Edit Staff") + |> assign(:user, user) + |> assign(:roles, roles) + end + + defp apply_action(socket, _live_action, _params) do + socket + end end diff --git a/lib/safira_web/live/backoffice/staff_live/index.html.heex b/lib/safira_web/live/backoffice/staff_live/index.html.heex index 110864353..62f488211 100644 --- a/lib/safira_web/live/backoffice/staff_live/index.html.heex +++ b/lib/safira_web/live/backoffice/staff_live/index.html.heex @@ -1,14 +1,6 @@ -<.page title="Staffs"> +<.page title={gettext("Staffs")}> <:actions>
- <.ensure_permissions user={@current_user} permissions={%{"staffs" => ["roles_edit"]}}> - <.link patch={~p"/dashboard/staffs/roles"}> - <.button> - <.icon name="hero-shield-exclamation" class="w-5 h-5" /> - - - - <.table_search id="staffs-table-name-search" params={@params} @@ -16,6 +8,20 @@ path={~p"/dashboard/staffs"} placeholder={gettext("Search for users")} /> + + <.ensure_permissions user={@current_user} permissions={%{"staffs" => ["edit"]}}> + <.link patch={~p"/dashboard/staffs/new"}> + <.button>New Staff + + + + <.ensure_permissions user={@current_user} permissions={%{"staffs" => ["roles_edit"]}}> + <.link patch={~p"/dashboard/staffs/roles"}> + <.button> + <.icon name="hero-shield-exclamation" class="w-5 h-5" /> + + +
@@ -61,45 +67,48 @@ <.modal - :if={@live_action in [:roles]} + :if={@live_action in [:new, :edit]} id="staff-modal" show on_cancel={JS.patch(~p"/dashboard/staffs")} > <.live_component - module={SafiraWeb.Backoffice.StaffLive.RoleLive.Index} - id="list-roles" - title="Roles" + module={SafiraWeb.Backoffice.StaffLive.FormComponent} + id={@user.id || :new} + title="Edit Staff" action={@live_action} patch={~p"/dashboard/staffs"} + current_user={@current_user} + user={@user} + roles={@roles} /> <.modal - :if={@live_action in [:roles_new, :roles_edit]} + :if={@live_action in [:roles]} id="staff-modal" show on_cancel={JS.patch(~p"/dashboard/staffs")} > <.live_component - module={SafiraWeb.Backoffice.StaffLive.RoleLive.FormComponent} - id={@params["id"] || "new-role"} - title="New Role" + module={SafiraWeb.Backoffice.StaffLive.RoleLive.Index} + id="list-roles" + title="Roles" action={@live_action} patch={~p"/dashboard/staffs"} /> <.modal - :if={@live_action in [:edit]} + :if={@live_action in [:roles_new, :roles_edit]} id="staff-modal" show on_cancel={JS.patch(~p"/dashboard/staffs")} > <.live_component - module={SafiraWeb.Backoffice.StaffLive.FormComponent} - id={@params["id"]} - title="Edit Staff" + module={SafiraWeb.Backoffice.StaffLive.RoleLive.FormComponent} + id={@params["id"] || "new-role"} + title="New Role" action={@live_action} patch={~p"/dashboard/staffs"} /> diff --git a/lib/safira_web/router.ex b/lib/safira_web/router.ex index 1ef0b54ca..f15f7bf54 100644 --- a/lib/safira_web/router.ex +++ b/lib/safira_web/router.ex @@ -194,6 +194,8 @@ defmodule SafiraWeb.Router do scope "/staffs", StaffLive do live "/", Index, :index + live "/new", Index, :new + live "/:id/edit", Index, :edit scope "/roles" do diff --git a/priv/repo/seeds/accounts.exs b/priv/repo/seeds/accounts.exs index 5b0e85170..4b8da4aee 100644 --- a/priv/repo/seeds/accounts.exs +++ b/priv/repo/seeds/accounts.exs @@ -75,17 +75,16 @@ defmodule Safira.Repo.Seeds.Accounts do handle = name |> String.downcase() |> String.replace(~r/\s/, "_") user = %{ - name: name, - handle: handle, - email: email, - password: "password1234" + "name" => name, + "handle" => handle, + "email" => email, + "password" => "password1234", + "staff" => %{ + "role_id" => Enum.at(roles, 0).id + } } - case Accounts.register_staff_user(user) do - {:ok, changeset} -> - user = Repo.update!(Accounts.User.confirm_changeset(changeset)) - Accounts.create_staff(%{user_id: user.id, role_id: Enum.random(roles).id}) - {:error, changeset} -> + with {:error, changeset} <- Accounts.register_staff_user(user) do Mix.shell().error(Kernel.inspect(changeset.errors)) end end diff --git a/test/safira/accounts_test.exs b/test/safira/accounts_test.exs index a55f76d17..0d8aefe1c 100644 --- a/test/safira/accounts_test.exs +++ b/test/safira/accounts_test.exs @@ -53,8 +53,9 @@ defmodule Safira.AccountsTest do {:error, _, changeset, _} = Accounts.register_attendee_user(%{}) assert %{ - password: ["can't be blank"], - email: ["can't be blank"] + email: ["can't be blank"], + handle: ["can't be blank"], + name: ["can't be blank"] } = errors_on(changeset) end @@ -105,17 +106,18 @@ defmodule Safira.AccountsTest do describe "register_staff_user/1" do test "requires email and password to be set" do - {:error, changeset} = Accounts.register_staff_user(%{}) + {:error, _, changeset, _} = Accounts.register_staff_user(%{}) assert %{ - password: ["can't be blank"], + handle: ["can't be blank"], + name: ["can't be blank"], email: ["can't be blank"] } = errors_on(changeset) end test "validates email and password when given" do - {:error, changeset} = - Accounts.register_staff_user(%{email: "not valid", password: "not valid"}) + {:error, _, changeset, _} = + Accounts.register_staff_user(%{"email" => "not valid", "password" => "not valid"}) assert %{ email: ["must have the @ sign and no spaces"], @@ -125,26 +127,12 @@ defmodule Safira.AccountsTest do test "validates maximum values for email and password for security" do too_long = String.duplicate("db", 100) - {:error, changeset} = Accounts.register_staff_user(%{email: too_long, password: too_long}) - assert "should be at most 160 character(s)" in errors_on(changeset).email - assert "should be at most 72 character(s)" in errors_on(changeset).password - end - test "validates email uniqueness" do - %{email: email} = user_fixture() - {:error, _changeset} = Accounts.register_staff_user(%{email: email}) - - # Now try with the upper cased email too, to check that email case is ignored. - {:error, _changeset} = Accounts.register_staff_user(%{email: String.upcase(email)}) - end + {:error, _, changeset, _} = + Accounts.register_staff_user(%{"email" => too_long, "password" => too_long}) - test "registers users with a hashed password" do - email = unique_user_email() - {:ok, user} = Accounts.register_staff_user(valid_user_attributes(email: email)) - assert user.email == email - assert is_binary(user.hashed_password) - assert is_nil(user.confirmed_at) - assert is_nil(user.password) + assert "should be at most 160 character(s)" in errors_on(changeset).email + assert "should be at most 72 character(s)" in errors_on(changeset).password end end