Skip to content

Commit

Permalink
use secret hash parameter to switch to admin mode (#485)
Browse files Browse the repository at this point in the history
  • Loading branch information
JannikStreek authored Nov 19, 2024
1 parent 10a1148 commit 82119d4
Show file tree
Hide file tree
Showing 32 changed files with 381 additions and 301 deletions.
82 changes: 38 additions & 44 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(){
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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
Expand Down
33 changes: 31 additions & 2 deletions assets/js/qrCodeUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const buildQrCodeOptions = (qrCodeUrl) => ({
import QRCodeStyling from "qr-code-styling";

const buildQrCodeOptions = (qrCodeUrl) => ({
backgroundOptions: {
color: "#fff",
},
Expand All @@ -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;
}
18 changes: 18 additions & 0 deletions assets/js/shareUtils.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
36 changes: 32 additions & 4 deletions lib/mindwendel/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/mindwendel/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 13 additions & 7 deletions lib/mindwendel/brainstormings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ defmodule Mindwendel.Brainstormings do
alias Mindwendel.Lanes
alias Mindwendel.Ideas
alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.Brainstormings.BrainstormingModeratingUser

require Logger

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions lib/mindwendel/brainstormings/brainstorming.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
])
Expand Down
35 changes: 0 additions & 35 deletions lib/mindwendel_web/channels/user_socket.ex

This file was deleted.

12 changes: 10 additions & 2 deletions lib/mindwendel_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -305,7 +309,11 @@ defmodule MindwendelWeb.CoreComponents do
end)

~H"""
<div class="form-check form-check-inline">
<div class={[
"form-check",
@options == [] && "form-check-inline",
@options == ["switch"] && "form-switch"
]}>
<label class="form-check-label">
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
Expand Down
4 changes: 0 additions & 4 deletions lib/mindwendel_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ defmodule MindwendelWeb.Endpoint do
signing_salt: "Kxo5qCGx"
]

socket "/socket", MindwendelWeb.UserSocket,
websocket: true,
longpoll: false

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

# Serve at "/" the static files from "priv/static" directory.
Expand Down
Loading

0 comments on commit 82119d4

Please sign in to comment.