From 716dbdddcbc2b95c631afbcfb54fcf4709508bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= <30907944+joaodiaslobo@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:08:31 +0000 Subject: [PATCH] fix: attendee credential association (#434) --- lib/safira/accounts.ex | 57 +++++++++++++++++++ .../components/layouts/app.html.heex | 2 +- lib/safira_web/helpers.ex | 4 +- .../live/app/credential_live/edit.ex | 48 ++++++++++++++++ .../credential_live/edit.html.heex} | 8 +-- .../live/app/credential_live/index.ex | 16 ++++++ .../live/app/credential_live/index.html.heex | 13 +++++ .../live/backoffice/scanner_live/index.ex | 33 ----------- lib/safira_web/live/credential_live/edit.ex | 37 ------------ .../live/credential_live/edit.html.heex | 17 ------ lib/safira_web/live/credential_live/index.ex | 16 ------ lib/safira_web/live/credential_live/show.ex | 44 -------------- .../live/credential_live/show.html.heex | 5 -- lib/safira_web/plugs/user_roles.ex | 30 ++++++++-- lib/safira_web/router.ex | 17 +++--- priv/repo/seeds/accounts.exs | 9 ++- 16 files changed, 182 insertions(+), 174 deletions(-) create mode 100644 lib/safira_web/live/app/credential_live/edit.ex rename lib/safira_web/live/{backoffice/scanner_live/index.html.heex => app/credential_live/edit.html.heex} (81%) create mode 100644 lib/safira_web/live/app/credential_live/index.ex create mode 100644 lib/safira_web/live/app/credential_live/index.html.heex delete mode 100644 lib/safira_web/live/backoffice/scanner_live/index.ex delete mode 100644 lib/safira_web/live/credential_live/edit.ex delete mode 100644 lib/safira_web/live/credential_live/edit.html.heex delete mode 100644 lib/safira_web/live/credential_live/index.ex delete mode 100644 lib/safira_web/live/credential_live/show.ex delete mode 100644 lib/safira_web/live/credential_live/show.html.heex diff --git a/lib/safira/accounts.ex b/lib/safira/accounts.ex index a2ec343d..82fc7725 100644 --- a/lib/safira/accounts.ex +++ b/lib/safira/accounts.ex @@ -585,6 +585,27 @@ defmodule Safira.Accounts do |> Repo.update() end + @doc """ + Links a credential to an attendee. + + ## Examples + + iex> link_credential(credential_id, attendee_id) + {:ok, %Credential{}} + + iex> link_credential(credential_id, attendee_id) + {:error, %Ecto.Changeset{}} + + """ + def link_credential(credential_id, attendee_id) do + credential = get_credential!(credential_id) + attendee = get_attendee!(attendee_id) + + credential + |> Credential.changeset(%{attendee_id: attendee.id}) + |> Repo.update() + end + @doc """ Deletes a credential. @@ -614,6 +635,42 @@ defmodule Safira.Accounts do Credential.changeset(credential, attrs) end + @doc """ + Checks if a credential exists. + + ## Examples + + iex> credential_exists?(123) + true + + iex> credential_exists?(456) + false + + """ + def credential_exists?(id) do + Credential + |> where([c], c.id == ^id) + |> Repo.exists?() + end + + @doc """ + Checks if a credential is linked to an attendee. + + ## Examples + + iex> credential_linked?(credential_id) + true + + iex> credential_linked?(credential_id) + false + + """ + def credential_linked?(credential_id) do + credential = get_credential!(credential_id) + + credential.attendee_id != nil + end + @doc """ Gets a single credential associated to the given attendee. diff --git a/lib/safira_web/components/layouts/app.html.heex b/lib/safira_web/components/layouts/app.html.heex index 38cd02e8..5a213407 100644 --- a/lib/safira_web/components/layouts/app.html.heex +++ b/lib/safira_web/components/layouts/app.html.heex @@ -26,7 +26,7 @@
-
+
<.flash_group flash={@flash} /> <%= @inner_content %>
diff --git a/lib/safira_web/helpers.ex b/lib/safira_web/helpers.ex index 9aaa1937..7971c6bd 100644 --- a/lib/safira_web/helpers.ex +++ b/lib/safira_web/helpers.ex @@ -18,7 +18,7 @@ defmodule SafiraWeb.Helpers do case URI.parse(url) do %URI{host: host, path: path} -> - if host == app_host or Mix.env() == :dev do + if (host == app_host or Mix.env() == :dev) and not is_nil(path) do case extract_id_from_url_path(path) do :error -> {:error, "not a valid id"} result -> result @@ -154,7 +154,7 @@ defmodule SafiraWeb.Helpers do end def draw_qr_code(qr_code) do - internal_route = "/qr_codes/#{qr_code.id}" + internal_route = "/app/attendees/#{qr_code.id}" url = build_url() <> internal_route url diff --git a/lib/safira_web/live/app/credential_live/edit.ex b/lib/safira_web/live/app/credential_live/edit.ex new file mode 100644 index 00000000..2b6c526d --- /dev/null +++ b/lib/safira_web/live/app/credential_live/edit.ex @@ -0,0 +1,48 @@ +defmodule SafiraWeb.App.CredentialLive.Edit do + use SafiraWeb, :app_view + + alias Safira.Accounts + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(_params, _url, socket) do + {:noreply, socket |> assign(:modal_data, nil)} + end + + @impl true + def handle_event("scan", data, socket) do + case safely_extract_id_from_url(data) do + {:ok, id} -> + if Accounts.credential_exists?(id) do + if Accounts.credential_linked?(id) do + {:noreply, socket |> assign(:modal_data, :already_linked)} + else + Accounts.link_credential(id, socket.assigns.current_user.attendee.id) + {:noreply, socket |> push_navigate(to: ~p"/app")} + end + else + {:noreply, socket |> assign(:modal_data, :not_found)} + end + + {:error, _} -> + {:noreply, socket |> assign(:modal_data, :invalid)} + end + end + + @impl true + def handle_event("close-modal", _, socket) do + {:noreply, socket |> assign(:modal_data, nil)} + end + + def error_message(:not_found), + do: gettext("This credential is not registered in the event's system! (404)") + + def error_message(:already_linked), + do: gettext("This credential is already linked to another attendee! (400)") + + def error_message(:invalid), do: gettext("Not a valid credential! (400)") +end diff --git a/lib/safira_web/live/backoffice/scanner_live/index.html.heex b/lib/safira_web/live/app/credential_live/edit.html.heex similarity index 81% rename from lib/safira_web/live/backoffice/scanner_live/index.html.heex rename to lib/safira_web/live/app/credential_live/edit.html.heex index 4d13fe05..69f052e5 100644 --- a/lib/safira_web/live/backoffice/scanner_live/index.html.heex +++ b/lib/safira_web/live/app/credential_live/edit.html.heex @@ -1,4 +1,4 @@ -<.page title="Identify an attendee"> +<.page title="Link Credential" size={:xl} title_class="font-terminal uppercase">
<.icon name="hero-x-circle" class="text-red-500 w-8" />

- <%= if @modal_data == :not_found do %> - <%= gettext("This credential is not associated with an attendee!") %> - <% else %> - <%= gettext("Not a valid credential!") %> + <%= if @modal_data do %> + <%= error_message(@modal_data) %> <% end %>

diff --git a/lib/safira_web/live/app/credential_live/index.ex b/lib/safira_web/live/app/credential_live/index.ex new file mode 100644 index 00000000..afb6fa46 --- /dev/null +++ b/lib/safira_web/live/app/credential_live/index.ex @@ -0,0 +1,16 @@ +defmodule SafiraWeb.App.CredentialLive.Index do + use SafiraWeb, :app_view + + alias Safira.Accounts + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:current_page, :credential) + |> assign( + :credential, + Accounts.get_credential_of_attendee!(socket.assigns.current_user.attendee) + )} + end +end diff --git a/lib/safira_web/live/app/credential_live/index.html.heex b/lib/safira_web/live/app/credential_live/index.html.heex new file mode 100644 index 00000000..bef6e498 --- /dev/null +++ b/lib/safira_web/live/app/credential_live/index.html.heex @@ -0,0 +1,13 @@ +<.page title="Credential" size={:xl} title_class="font-terminal uppercase"> +

+ <%= gettext( + "The code below is used to identify you in case you are missing your physical credential." + ) %> +

+
+ <%= draw_qr_code(@credential) |> raw %> +

+ <%= @current_user.name %> +

+
+ diff --git a/lib/safira_web/live/backoffice/scanner_live/index.ex b/lib/safira_web/live/backoffice/scanner_live/index.ex deleted file mode 100644 index 37d7b69d..00000000 --- a/lib/safira_web/live/backoffice/scanner_live/index.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule SafiraWeb.ScannerLive.Index do - use SafiraWeb, :backoffice_view - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(_params, _url, socket) do - {:noreply, socket |> assign(:modal_data, nil)} - end - - @impl true - def handle_event("scan", data, socket) do - case safely_extract_id_from_url(data) do - {:ok, id} -> - if Safira.Accounts.attendee_exists?(id) do - {:noreply, socket |> push_navigate(to: ~p"/dashboard/attendees/#{id}")} - else - {:noreply, socket |> assign(:modal_data, :not_found)} - end - - {:error, _} -> - {:noreply, socket |> assign(:modal_data, :invalid)} - end - end - - @impl true - def handle_event("close-modal", _, socket) do - {:noreply, socket |> assign(:modal_data, nil)} - end -end diff --git a/lib/safira_web/live/credential_live/edit.ex b/lib/safira_web/live/credential_live/edit.ex deleted file mode 100644 index b69e200d..00000000 --- a/lib/safira_web/live/credential_live/edit.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule SafiraWeb.App.CredentialLive.Edit do - use SafiraWeb, :app_view - - alias Safira.Accounts - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(%{"id" => id}, _, socket) do - credential = Accounts.get_credential!(id, [:attendee]) - - if is_nil(credential.attendee) do - {:noreply, socket |> assign(credential: credential)} - else - {:noreply, socket |> push_navigate(to: ~p"/app/credential/#{credential.id}")} - end - end - - @impl true - def handle_event("claim", _, socket) do - socket.assigns.credential - |> Accounts.update_credential(%{attendee_id: socket.assigns.current_user.attendee.id}) - |> case do - {:ok, _credential} -> - {:noreply, - socket - |> put_flash(:info, "Credential claimed successfully") - |> push_navigate(to: ~p"/app/")} - - {:error, _changeset} -> - {:noreply, socket |> put_flash(:error, "Unable to claim credential. Try again later")} - end - end -end diff --git a/lib/safira_web/live/credential_live/edit.html.heex b/lib/safira_web/live/credential_live/edit.html.heex deleted file mode 100644 index 4d01bb8d..00000000 --- a/lib/safira_web/live/credential_live/edit.html.heex +++ /dev/null @@ -1,17 +0,0 @@ -<.page title="Claim Credential" size={:xl} title_class="font-terminal uppercase"> -
- <%= draw_qr_code(@credential) |> raw %> -
- -

- You are about to claim the scanned credential. This action cannot be undone. You can have multiple credentials claimed to your account. -

- - <.action_button - title="Claim Credential" - subtitle="" - class="w-full mt-8 max-w-96 block mx-auto w-fit" - disabled={false} - phx-click="claim" - /> - diff --git a/lib/safira_web/live/credential_live/index.ex b/lib/safira_web/live/credential_live/index.ex deleted file mode 100644 index 50818da4..00000000 --- a/lib/safira_web/live/credential_live/index.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule SafiraWeb.App.CredentialLive.Index do - use SafiraWeb, :app_view - - alias Safira.Accounts - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(_, _, socket) do - credential = Accounts.get_credential_of_attendee!(socket.assigns.current_user.attendee) - {:noreply, socket |> push_navigate(to: ~p"/app/credential/#{credential.id}")} - end -end diff --git a/lib/safira_web/live/credential_live/show.ex b/lib/safira_web/live/credential_live/show.ex deleted file mode 100644 index b6defdf7..00000000 --- a/lib/safira_web/live/credential_live/show.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule SafiraWeb.App.CredentialLive.Show do - use SafiraWeb, :app_view - - alias Safira.Accounts - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(%{"id" => id}, _, socket) do - credential = Accounts.get_credential!(id, [:attendee]) - - # QR Code not yet assigned - if is_nil(credential.attendee) do - # User is not an attendee, show nothing - # User is an attendee, redirect to assigning page - if is_nil(socket.assigns.current_user.attendee.id) do - {:noreply, - socket - |> push_navigate(to: "/404", replace: true)} - else - {:noreply, - socket - |> push_navigate(to: ~p"/app/credential/#{credential.id}/edit", replace: true)} - end - else - # Current user is the attendee which the QR Code belongs to, display - # the QR code - # Redirect to the user profile - if credential.attendee_id == socket.assigns.current_user.attendee.id do - {:noreply, - socket - |> assign(:page_title, "Show QR Code") - |> assign(:credential, credential)} - else - {:noreply, - socket - |> push_navigate(to: "/attendees/#{credential.attendee_id}", replace: true)} - end - end - end -end diff --git a/lib/safira_web/live/credential_live/show.html.heex b/lib/safira_web/live/credential_live/show.html.heex deleted file mode 100644 index 807f6f9c..00000000 --- a/lib/safira_web/live/credential_live/show.html.heex +++ /dev/null @@ -1,5 +0,0 @@ -<.page title="Credential" size={:xl} title_class="font-terminal uppercase"> -
- <%= draw_qr_code(@credential) |> raw %> -
- diff --git a/lib/safira_web/plugs/user_roles.ex b/lib/safira_web/plugs/user_roles.ex index ab304c25..09d4f2da 100644 --- a/lib/safira_web/plugs/user_roles.ex +++ b/lib/safira_web/plugs/user_roles.ex @@ -4,26 +4,48 @@ defmodule SafiraWeb.UserRoles do """ use SafiraWeb, :verified_routes + import SafiraWeb.Gettext + import Plug.Conn import Phoenix.Controller alias Safira.Accounts def require_credential(conn, _opts) do - if is_nil(conn.assigns.current_user.attendee) or - not is_nil(Accounts.get_credential_of_attendee(conn.assigns.current_user.attendee)) do + if has_credential?(conn) do conn else conn |> put_flash( :error, - "You haven't assigned a credential to your account. You need one to participate in SEI" + gettext( + "You haven't assigned a credential to your account. You need one to participate in the event." + ) ) - |> redirect(to: ~p"/scanner") + |> redirect(to: ~p"/app/credential/link") |> halt() end end + def require_no_credential(conn, _opts) do + if has_credential?(conn) do + conn + |> put_flash( + :error, + gettext("You already have a credential assigned to your account.") + ) + |> redirect(to: ~p"/app") + |> halt() + else + conn + end + end + + defp has_credential?(conn) do + is_nil(conn.assigns.current_user.attendee) or + not is_nil(Accounts.get_credential_of_attendee(conn.assigns.current_user.attendee)) + end + @doc """ Used for routes that require the user to be an attendee. """ diff --git a/lib/safira_web/router.ex b/lib/safira_web/router.ex index 3da0b0de..558e94d2 100644 --- a/lib/safira_web/router.ex +++ b/lib/safira_web/router.ex @@ -73,9 +73,18 @@ defmodule SafiraWeb.Router do live "/scanner", ScannerLive.Index, :index scope "/app", App do - pipe_through [:require_attendee_user, :require_credential] + pipe_through [:require_attendee_user] + + scope "/credential", CredentialLive do + pipe_through [:require_no_credential] + live "/link", Edit, :edit + end + + pipe_through [:require_credential] live "/", HomeLive.Index, :index + live "/credential", CredentialLive.Index, :index + live "/wheel", WheelLive.Index, :index scope "/store", StoreLive do @@ -83,12 +92,6 @@ defmodule SafiraWeb.Router do live "/product/:id", Show, :show end - scope "/credential", CredentialLive do - live "/", Index, :index - live "/:id", Show, :show - live "/:id/edit", Edit, :edit - end - live "/vault", VaultLive.Index, :index end diff --git a/priv/repo/seeds/accounts.exs b/priv/repo/seeds/accounts.exs index 7f6a653f..fbcfc524 100644 --- a/priv/repo/seeds/accounts.exs +++ b/priv/repo/seeds/accounts.exs @@ -26,7 +26,7 @@ defmodule Safira.Repo.Seeds.Accounts do case Accounts.list_credentials() do [] -> - seed_credentials(credential_count) + seed_credentials(credential_count, div(attendee_names |> length(), 2)) _ -> Mix.shell().erroring("Found credentials, aborting seeding credentials.") end @@ -78,9 +78,12 @@ defmodule Safira.Repo.Seeds.Accounts do end end - def seed_credentials(credential_count) do + def seed_credentials(credential_count, attendee_to_link_count) do + attendees = Accounts.list_attendees() + for i <- 0..credential_count do - Accounts.create_credential(%{attendee_id: nil}) + id = if i < attendee_to_link_count do Enum.at(attendees, i).attendee.id else nil end + Accounts.create_credential(%{attendee_id: id}) end end end