From 82119d4b2857778358478b4a450e2e4a1d960a59 Mon Sep 17 00:00:00 2001 From: JannikStreek Date: Tue, 19 Nov 2024 11:00:45 +0100 Subject: [PATCH] use secret hash parameter to switch to admin mode (#485) --- assets/js/app.js | 82 +++++++++---------- assets/js/qrCodeUtils.js | 33 +++++++- assets/js/shareUtils.js | 18 ++++ lib/mindwendel/accounts.ex | 36 +++++++- .../brainstorming_moderating_user.ex | 2 +- lib/mindwendel/accounts/user.ex | 2 +- lib/mindwendel/brainstormings.ex | 20 +++-- .../brainstormings/brainstorming.ex | 4 +- lib/mindwendel_web/channels/user_socket.ex | 35 -------- .../components/core_components.ex | 12 ++- lib/mindwendel_web/endpoint.ex | 4 - .../admin/brainstorming_live/edit.html.heex | 17 +--- .../brainstorming_live/share_component.ex | 24 ++++++ .../share_component.html.heex | 14 ++++ .../live/brainstorming_live/show.ex | 13 ++- .../live/brainstorming_live/show.html.heex | 2 +- .../live/idea_live/card_component.ex | 2 +- .../live/label_live/captions_component.ex | 3 +- lib/mindwendel_web/live/live_helpers.ex | 2 +- priv/gettext/de/LC_MESSAGES/default.po | 71 ++++++++-------- priv/gettext/default.pot | 71 ++++++++-------- priv/gettext/en/LC_MESSAGES/default.po | 71 ++++++++-------- test/mindwendel/accounts_test.exs | 50 ++++++++++- .../create_brainstorming_test.exs | 2 +- test/mindwendel/brainstormings_test.exs | 43 +++------- .../show_idea_comment_test.exs | 4 +- .../show_idea_delete_test.exs | 5 +- .../show_idea_edit_test.exs | 10 +-- .../show_sort_by_label_test.exs | 4 +- .../live/brainstorming_live_test.exs | 11 +-- .../live/label_live/captions_test.exs | 12 +-- test/support/factory.ex | 3 +- 32 files changed, 381 insertions(+), 301 deletions(-) create mode 100644 assets/js/shareUtils.js rename lib/mindwendel/{brainstormings => accounts}/brainstorming_moderating_user.ex (91%) delete mode 100644 lib/mindwendel_web/channels/user_socket.ex diff --git a/assets/js/app.js b/assets/js/app.js index bf4af30f..aa76c854 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -20,39 +20,15 @@ import "phoenix_html" import { Socket } from "phoenix" import NProgress from "nprogress" import { LiveSocket } from "phoenix_live_view" -import QRCodeStyling from "qr-code-styling"; import ClipboardJS from "clipboard" -import { buildQrCodeOptions } from "./qrCodeUtils.js" +import { appendQrCode, initQrDownload } from "./qrCodeUtils.js" +import { initShareButtonClickHandler } from "./shareUtils.js" let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let Hooks = {} const sortables = []; -Hooks.CopyBrainstormingLinkButton = { - mounted() { - new ClipboardJS(this.el); - } -} - -Hooks.NativeSharingButton = { - mounted() { - const shareData = { - title: this.el.getAttribute(`data-native-sharing-button-share-data-title`) || 'Mindwendel Brainstorming', - text: this.el.getAttribute(`data-native-sharing-button-share-data-text`) || 'Join my brainstorming', - url: this.el.getAttribute(`data-native-sharing-button-share-data-url`) || document.getElementById("brainstorming-link").value - } - - if (navigator.share) { - this.el.addEventListener('click', (event) => { - navigator.share(shareData) - .then() // Do nothing - .catch(err => { console.log(`Error: ${err}`) }) - }) - } - } -} - // see https://github.com/drag-drop-touch-js/dragdroptouch for mobile support Hooks.Sortable = { mounted(){ @@ -89,32 +65,49 @@ Hooks.Modal = { window.addEventListener('mindwendel:hide-modal', closeModal); } } -Hooks.QrCodeCanvas = { + +Hooks.CopyBrainstormingLinkButton = { mounted() { - const qrCodeCanvasElement = this.el - const qrCodeUrl = qrCodeCanvasElement.getAttribute("data-qr-code-url") + new ClipboardJS(this.el); + } +} - const qrCodeOptions = buildQrCodeOptions(qrCodeUrl) - const qrCode = new QRCodeStyling(qrCodeOptions) +let refShareClickListenerFunction; +let refShareButton; - qrCode.append(qrCodeCanvasElement); +Hooks.NativeSharingButton = { + mounted() { + refShareButton = this.el; + refShareClickListenerFunction = initShareButtonClickHandler(refShareButton); + }, + updated() { + refShareButton.removeEventListener("click", refShareClickListenerFunction); + refShareButton = this.el; + refShareClickListenerFunction = initShareButtonClickHandler(refShareButton); } } -Hooks.QrCodeDownloadButton = { +Hooks.QrCodeCanvas = { mounted() { - const qrCodeUrl = this.el.getAttribute("data-qr-code-url"); - const qrCodeFilename = this.el.getAttribute("data-qr-code-filename") || qrCodeUrl || "qrcode"; - const qrCodeFileExtension = this.el.getAttribute("data-qr-code-file-extension") || "png"; + appendQrCode(this.el); + }, + updated() { + appendQrCode(this.el); + } +} - const qrCodeOptions = buildQrCodeOptions(qrCodeUrl) - const qrCode = new QRCodeStyling(qrCodeOptions) +let refQrClickListenerFunction; +let refQrCodeDownloadButton; - this.el && this.el.addEventListener('click', () => { - qrCode.download({ name: qrCodeFilename, extension: qrCodeFileExtension }) - .then() // Do nothing - .catch(err => { console.log(`Error: ${err}`) }) - }) +Hooks.QrCodeDownloadButton = { + mounted() { + refQrCodeDownloadButton = this.el; + refQrClickListenerFunction = initQrDownload(refQrCodeDownloadButton); + }, + updated() { + refQrCodeDownloadButton.removeEventListener("click", refQrClickListenerFunction); + refQrCodeDownloadButton = this.el; + refQrClickListenerFunction = initQrDownload(refQrCodeDownloadButton); } } @@ -138,8 +131,9 @@ Hooks.SetIdeaLabelBackgroundColor = { } }; +// The brainstorming secret from the url ("#123") is added as well to the socket. The secret is not available on the server side by default. let liveSocket = new LiveSocket("/live", Socket, { - hooks: Hooks, params: { _csrf_token: csrfToken } + hooks: Hooks, params: { _csrf_token: csrfToken, adminSecret: window.location.hash.substring(1) } }) // Show progress bar on live navigation and form submits diff --git a/assets/js/qrCodeUtils.js b/assets/js/qrCodeUtils.js index d128040a..c2083636 100644 --- a/assets/js/qrCodeUtils.js +++ b/assets/js/qrCodeUtils.js @@ -1,4 +1,6 @@ -export const buildQrCodeOptions = (qrCodeUrl) => ({ +import QRCodeStyling from "qr-code-styling"; + +const buildQrCodeOptions = (qrCodeUrl) => ({ backgroundOptions: { color: "#fff", }, @@ -20,4 +22,31 @@ export const buildQrCodeOptions = (qrCodeUrl) => ({ height: 300, type: "svg", width: 300 -}) +}); + +export const appendQrCode = (qrCodeCanvasElement) => { + const qrCodeUrl = qrCodeCanvasElement.getAttribute("data-qr-code-url") + + const qrCodeOptions = buildQrCodeOptions(qrCodeUrl) + const qrCode = new QRCodeStyling(qrCodeOptions) + + qrCode.append(qrCodeCanvasElement); +} + +export const initQrDownload = (button) => { + const qrCodeUrl = button.getAttribute("data-qr-code-url"); + const qrCodeFilename = button.getAttribute("data-qr-code-filename") || qrCodeUrl || "qrcode"; + const qrCodeFileExtension = button.getAttribute("data-qr-code-file-extension") || "png"; + + const qrCodeOptions = buildQrCodeOptions(qrCodeUrl) + const qrCode = new QRCodeStyling(qrCodeOptions) + + const clickEventListener = () => { + qrCode.download({ name: qrCodeFilename, extension: qrCodeFileExtension }) + .then() // Do nothing + .catch(err => { console.log(`Error: ${err}`) }) + }; + + button && button.addEventListener('click', clickEventListener); + return clickEventListener; +} diff --git a/assets/js/shareUtils.js b/assets/js/shareUtils.js new file mode 100644 index 00000000..3e0d81f5 --- /dev/null +++ b/assets/js/shareUtils.js @@ -0,0 +1,18 @@ +export const initShareButtonClickHandler = (button) => { + const shareData = { + title: button.getAttribute(`data-native-sharing-button-share-data-title`) || 'Mindwendel Brainstorming', + text: button.getAttribute(`data-native-sharing-button-share-data-text`) || 'Join my brainstorming', + url: document.getElementById("data-native-sharing-button-share-data-url") || document.getElementById("brainstorming-link-input-readonly").value + } + + const clickHandler = (_event) => { + navigator.share(shareData) + .then() // Do nothing + .catch(err => { console.log(`Error: ${err}`) }) + } + + if (navigator.share) { + button.addEventListener('click', clickHandler); + return clickHandler; + } +} \ No newline at end of file diff --git a/lib/mindwendel/accounts.ex b/lib/mindwendel/accounts.ex index 6a28459f..76d4f877 100644 --- a/lib/mindwendel/accounts.ex +++ b/lib/mindwendel/accounts.ex @@ -3,6 +3,7 @@ defmodule Mindwendel.Accounts do alias Mindwendel.Repo alias Mindwendel.Accounts.User alias Mindwendel.Brainstormings.Brainstorming + alias Mindwendel.Accounts.BrainstormingModeratingUser require Logger @@ -47,7 +48,7 @@ defmodule Mindwendel.Accounts do end def get_user(id) do - Repo.get(User, id) |> Repo.preload(:brainstormings) + Repo.get(User, id) |> Repo.preload([:brainstormings, :moderated_brainstormings]) rescue Ecto.Query.CastError -> nil end @@ -70,6 +71,36 @@ defmodule Mindwendel.Accounts do |> Repo.update() end + @doc """ + Adds a user as moderating user to a brainstorming. + + ## Examples + + iex> add_moderating_user(brainstorming, user) + %Brainstorming{} + + """ + def add_moderating_user(%Brainstorming{} = brainstorming, %User{} = user) do + if user.id in Enum.map(brainstorming.moderating_users, fn e -> e.id end) do + {:error} + else + %BrainstormingModeratingUser{brainstorming_id: brainstorming.id, user_id: user.id} + |> BrainstormingModeratingUser.changeset() + |> Repo.insert() + end + end + + def add_moderating_user(%Brainstorming{} = brainstorming, user_id) when is_binary(user_id) do + case Ecto.UUID.dump(user_id) do + :error -> {:error} + {:ok, _} -> add_moderating_user(brainstorming, get_or_create_user(user_id)) + end + end + + def add_moderating_user(%Brainstorming{} = _brainstorming, user_id) when is_nil(user_id) do + {:error} + end + @doc """ Connects user to a brainstorm. @@ -80,9 +111,6 @@ defmodule Mindwendel.Accounts do iex> merge_brainstorming_user(brainstorming, user) %Brainstorming{} - iex> update_user(user, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ def merge_brainstorming_user(%Brainstorming{} = brainstorming, %User{} = user) do # credo:disable-for-next-line diff --git a/lib/mindwendel/brainstormings/brainstorming_moderating_user.ex b/lib/mindwendel/accounts/brainstorming_moderating_user.ex similarity index 91% rename from lib/mindwendel/brainstormings/brainstorming_moderating_user.ex rename to lib/mindwendel/accounts/brainstorming_moderating_user.ex index c36f204f..1c37db36 100644 --- a/lib/mindwendel/brainstormings/brainstorming_moderating_user.ex +++ b/lib/mindwendel/accounts/brainstorming_moderating_user.ex @@ -1,4 +1,4 @@ -defmodule Mindwendel.Brainstormings.BrainstormingModeratingUser do +defmodule Mindwendel.Accounts.BrainstormingModeratingUser do # Not using Mindwendel.Schema as the `@derive` in there clashes here use Ecto.Schema alias Mindwendel.Brainstormings.Brainstorming diff --git a/lib/mindwendel/accounts/user.ex b/lib/mindwendel/accounts/user.ex index ea4168d4..a40ccb96 100644 --- a/lib/mindwendel/accounts/user.ex +++ b/lib/mindwendel/accounts/user.ex @@ -5,7 +5,7 @@ defmodule Mindwendel.Accounts.User do alias Mindwendel.Brainstormings.Brainstorming alias Mindwendel.Brainstormings.Idea - alias Mindwendel.Brainstormings.BrainstormingModeratingUser + alias Mindwendel.Accounts.BrainstormingModeratingUser alias Mindwendel.Accounts.BrainstormingUser schema "users" do diff --git a/lib/mindwendel/brainstormings.ex b/lib/mindwendel/brainstormings.ex index 642778e8..542939ca 100644 --- a/lib/mindwendel/brainstormings.ex +++ b/lib/mindwendel/brainstormings.ex @@ -13,7 +13,6 @@ defmodule Mindwendel.Brainstormings do alias Mindwendel.Lanes alias Mindwendel.Ideas alias Mindwendel.Brainstormings.Brainstorming - alias Mindwendel.Brainstormings.BrainstormingModeratingUser require Logger @@ -36,12 +35,6 @@ defmodule Mindwendel.Brainstormings do ) end - def add_moderating_user(%Brainstorming{} = brainstorming, %User{} = user) do - %BrainstormingModeratingUser{brainstorming_id: brainstorming.id, user_id: user.id} - |> BrainstormingModeratingUser.changeset() - |> Repo.insert() - end - @doc """ Returns the list of brainstormings. @@ -237,6 +230,19 @@ defmodule Mindwendel.Brainstormings do brainstorming end + @doc """ + Validates the given secret against the brainstorming. Returns true/false. + + ## Examples + + iex> validate_admin_secret(brainstorming, abc) + false + + """ + def validate_admin_secret(brainstorming, admin_secret_id) do + brainstorming.admin_url_id == admin_secret_id + end + @doc """ Returns a subscibe result. diff --git a/lib/mindwendel/brainstormings/brainstorming.ex b/lib/mindwendel/brainstormings/brainstorming.ex index 80ee5152..6fb176ea 100644 --- a/lib/mindwendel/brainstormings/brainstorming.ex +++ b/lib/mindwendel/brainstormings/brainstorming.ex @@ -6,13 +6,12 @@ defmodule Mindwendel.Brainstormings.Brainstorming do alias Mindwendel.Brainstormings.Idea alias Mindwendel.Brainstormings.IdeaLabel alias Mindwendel.Brainstormings.Lane - alias Mindwendel.Brainstormings.BrainstormingModeratingUser + alias Mindwendel.Accounts.BrainstormingModeratingUser alias Mindwendel.Accounts.User alias Mindwendel.Accounts.BrainstormingUser schema "brainstormings" do field :name, :string - field :option_show_link_to_settings, :boolean field :option_allow_manual_ordering, :boolean # credo:disable-for-next-line # Todo: The following line can be changed `field :admin_url_id, Ecto.UUID, autogenerate: true` @@ -35,7 +34,6 @@ defmodule Mindwendel.Brainstormings.Brainstorming do brainstorming |> cast(attrs, [ :name, - :option_show_link_to_settings, :option_allow_manual_ordering, :filter_labels_ids ]) diff --git a/lib/mindwendel_web/channels/user_socket.ex b/lib/mindwendel_web/channels/user_socket.ex deleted file mode 100644 index 22ec171d..00000000 --- a/lib/mindwendel_web/channels/user_socket.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule MindwendelWeb.UserSocket do - use Phoenix.Socket - - ## Channels - # channel "room:*", MindwendelWeb.RoomChannel - - # Socket params are passed from the client and can - # be used to verify and authenticate a user. After - # verification, you can put default assigns into - # the socket that will be set for all channels, ie - # - # {:ok, assign(socket, :user_id, verified_user_id)} - # - # To deny connection, return `:error`. - # - # See `Phoenix.Token` documentation for examples in - # performing token verification on connect. - @impl true - def connect(_params, socket, _connect_info) do - {:ok, socket} - end - - # Socket id's are topics that allow you to identify all sockets for a given user: - # - # def id(socket), do: "user_socket:#{socket.assigns.user_id}" - # - # Would allow you to broadcast a "disconnect" event and terminate - # all active sockets and channels for a given user: - # - # MindwendelWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) - # - # Returning `nil` makes this socket anonymous. - @impl true - def id(_socket), do: nil -end diff --git a/lib/mindwendel_web/components/core_components.ex b/lib/mindwendel_web/components/core_components.ex index d72b6dac..8bfe0e62 100644 --- a/lib/mindwendel_web/components/core_components.ex +++ b/lib/mindwendel_web/components/core_components.ex @@ -280,7 +280,11 @@ defmodule MindwendelWeb.CoreComponents do attr :errors, :list, default: [] attr :checked, :boolean, doc: "the checked flag for checkbox inputs" attr :prompt, :string, default: nil, doc: "the prompt for select inputs" - attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" + + attr :options, :list, + default: [], + doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" + attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs" attr :rest, :global, @@ -305,7 +309,11 @@ defmodule MindwendelWeb.CoreComponents do end) ~H""" -
+