From e3fa55c1ab332605eca24ca33b5e37bfc5c5140a Mon Sep 17 00:00:00 2001 From: Adam Bellinson Date: Wed, 12 Jun 2019 16:34:51 -0400 Subject: [PATCH] permissions. (#28) * we are on our way * wip * fixed create board * wip * . * wip * . * . * yay * ok --- .iex.exs | 2 + TODO.md | 11 ++ lib/lucidboard/account.ex | 50 +++++++- lib/lucidboard/ecto_enums.ex | 2 + lib/lucidboard/live_board/agent.ex | 4 +- lib/lucidboard/schema/board.ex | 9 +- lib/lucidboard/schema/board_role.ex | 21 ++++ lib/lucidboard/schema/card.ex | 8 +- lib/lucidboard/schema/user.ex | 3 +- lib/lucidboard/seeds.ex | 6 +- lib/lucidboard/twiddler.ex | 48 +++++-- lib/lucidboard/twiddler/actions.ex | 118 ++++++++++++------ lib/lucidboard/twiddler/op.ex | 8 +- .../controllers/board_controller.ex | 3 +- lib/lucidboard_web/live/board_live.ex | 22 ++++ lib/lucidboard_web/live/dashboard_live.ex | 3 +- .../templates/board/options.html.eex | 44 ++++++- .../layout/header_user_menu.html.eex | 6 +- lib/lucidboard_web/view_helper.ex | 24 +++- mix.exs | 3 +- mix.lock | 15 +-- .../migrations/20180507203357_initial.exs | 9 +- test/lucidboard/live_board_test.exs | 11 +- test/lucidboard/twiddler_test.exs | 33 +++-- test/lucidboard_web/view_helper_test.exs | 32 +++++ 25 files changed, 398 insertions(+), 97 deletions(-) create mode 100644 lib/lucidboard/ecto_enums.ex create mode 100644 lib/lucidboard/schema/board_role.ex create mode 100644 test/lucidboard_web/view_helper_test.exs diff --git a/.iex.exs b/.iex.exs index a170e60..9072277 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,7 +1,9 @@ import Ecto.Query alias Ecto.{Changeset, UUID} alias Lucidboard.{ + Account, Board, + BoardRole, BoardSettings, Column, Card, diff --git a/TODO.md b/TODO.md index 061a021..6a73a85 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,8 @@ Lucidboard Features - investigate db indexes +- investigate db indexes + ## Needed before pilot ### Backend @@ -55,3 +57,12 @@ auto groups starred board timer maybe ? search + +## Permission notes + +- Roles + - Observer + - Contributor + - Owner +--------------------------- + - Admin? (Global) diff --git a/lib/lucidboard/account.ex b/lib/lucidboard/account.ex index eb67b11..dbb43ae 100644 --- a/lib/lucidboard/account.ex +++ b/lib/lucidboard/account.ex @@ -1,8 +1,9 @@ defmodule Lucidboard.Account do @moduledoc "Context for user things" import Ecto.Query + alias Ecto.Changeset alias Lucidboard.Account.Github - alias Lucidboard.{Repo, User} + alias Lucidboard.{Board, BoardRole, Repo, User} alias Ueberauth.Auth require Logger @@ -18,6 +19,53 @@ defmodule Lucidboard.Account do Repo.get(User, user_id) end + def display_name(%User{name: name, full_name: full_name}) do + "#{name} (#{full_name})" + end + + @spec has_role?(User.t(), Board.t(), atom) :: boolean + def has_role?(%User{id: user_id}, %Board{board_roles: roles}, role \\ :owner) do + Enum.any?(roles, fn + %{user_id: ^user_id, role: ^role} -> true + _ -> false + end) + end + + @spec suggest_users(String.t()) :: [User.t()] + def suggest_users(query) do + q = "%#{query}%" + + Repo.all( + from(u in User, where: ilike(u.name, ^q) or ilike(u.full_name, ^q)) + ) + end + + @spec grant(integer, BoardRole.t()) :: :ok | :error + def grant(board_id, board_role) do + with %Board{} = board <- + Board |> Repo.get(board_id) |> Repo.preload(:board_roles), + {:ok, _} <- + board + |> Board.changeset() + |> Changeset.put_assoc(:board_roles, [board_role | board.board_roles]) + |> Repo.update() do + :ok + else + _ -> :error + end + end + + @spec revoke(integer, integer) :: :ok + def revoke(user_id, board_id) do + Repo.delete_all( + from(r in BoardRole, + where: r.user_id == ^user_id and r.board_id == ^board_id + ) + ) + + :ok + end + @doc """ Given the `%Ueberauth.Auth{}` result, get a loaded user from the db. diff --git a/lib/lucidboard/ecto_enums.ex b/lib/lucidboard/ecto_enums.ex new file mode 100644 index 0000000..1b888d0 --- /dev/null +++ b/lib/lucidboard/ecto_enums.ex @@ -0,0 +1,2 @@ +import EctoEnum +defenum(BoardRoleEnum, owner: 0, contributor: 1, observer: 2) diff --git a/lib/lucidboard/live_board/agent.ex b/lib/lucidboard/live_board/agent.ex index a69c2fb..8f81cbc 100644 --- a/lib/lucidboard/live_board/agent.ex +++ b/lib/lucidboard/live_board/agent.ex @@ -41,7 +41,7 @@ defmodule Lucidboard.LiveBoard.Agent do @impl true def handle_call({:action, action, opts}, _from, state) when is_list(opts) do - case Twiddler.act(state.board, action) do + case Twiddler.act(state.board, action, opts) do {:ok, new_board, tx_fn, meta, event} -> user = Keyword.get(opts, :user) {event, events} = add_event(state.events, event, new_board, user) @@ -54,7 +54,7 @@ defmodule Lucidboard.LiveBoard.Agent do Scribe.write(new_board.id, [ tx_fn, - (if event, do: fn -> TimeMachine.commit(event) end) + if(event, do: fn -> TimeMachine.commit(event) end) ]) ret = diff --git a/lib/lucidboard/schema/board.ex b/lib/lucidboard/schema/board.ex index 32a78f6..0092f6d 100644 --- a/lib/lucidboard/schema/board.ex +++ b/lib/lucidboard/schema/board.ex @@ -10,7 +10,7 @@ defmodule Lucidboard.Board do @moduledoc "Schema for a board record" use Ecto.Schema import Ecto.Changeset - alias Lucidboard.{BoardSettings, Column, Event, User} + alias Lucidboard.{BoardRole, BoardSettings, Column, Event, User} @derive {Jason.Encoder, only: ~w(id title settings columns)a} @@ -20,6 +20,7 @@ defmodule Lucidboard.Board do has_many(:columns, Column) has_many(:events, Event) belongs_to(:user, User) + has_many(:board_roles, BoardRole) field(:inserted_at, :utc_datetime) field(:updated_at, :utc_datetime) @@ -39,11 +40,11 @@ defmodule Lucidboard.Board do end @doc false - def changeset(board, attrs) do + def changeset(board, attrs \\ %{}) do board |> cast(attrs, [:title]) - |> put_change(:settings, attrs.settings) - |> cast_assoc(:columns) |> validate_required([:title]) + |> cast_assoc(:columns) + |> cast_embed(:settings) end end diff --git a/lib/lucidboard/schema/board_role.ex b/lib/lucidboard/schema/board_role.ex new file mode 100644 index 0000000..907d9d9 --- /dev/null +++ b/lib/lucidboard/schema/board_role.ex @@ -0,0 +1,21 @@ +defmodule Lucidboard.BoardRole do + @moduledoc "Schema for role a user has on a board" + use Ecto.Schema + alias Ecto.UUID + alias Lucidboard.{Board, User} + + @primary_key {:id, :binary_id, autogenerate: false} + # @derive {Jason.Encoder, only: ~w(id)a} + + schema "board_roles" do + belongs_to(:board, Board) + belongs_to(:user, User) + field(:role, BoardRoleEnum) + end + + @spec new(keyword) :: Like.t() + def new(fields \\ []) do + defaults = [id: UUID.generate()] + struct(__MODULE__, Keyword.merge(defaults, fields)) + end +end diff --git a/lib/lucidboard/schema/card.ex b/lib/lucidboard/schema/card.ex index c35f945..046574e 100644 --- a/lib/lucidboard/schema/card.ex +++ b/lib/lucidboard/schema/card.ex @@ -36,12 +36,7 @@ defmodule Lucidboard.Card do struct(__MODULE__, Keyword.merge(defaults, fields)) end - def changeset(card) do - card - |> cast(%{}, [:body, :pile_id, :pos]) - end - - def changeset(card, attrs) do + def changeset(card, attrs \\ %{}) do settings = if attrs["color"] do %{color: attrs["color"]} @@ -57,7 +52,6 @@ defmodule Lucidboard.Card do card |> cast(attrs, [:body, :pile_id, :pos]) end - end @doc "Get the number of likes on a card" diff --git a/lib/lucidboard/schema/user.ex b/lib/lucidboard/schema/user.ex index 7bb91d0..8ee0924 100644 --- a/lib/lucidboard/schema/user.ex +++ b/lib/lucidboard/schema/user.ex @@ -2,7 +2,7 @@ defmodule Lucidboard.User do @moduledoc "Schema for a board record" use Ecto.Schema import Ecto.Changeset - alias Lucidboard.{Card, Like, UserSettings} + alias Lucidboard.{BoardRole, Card, Like, UserSettings} schema "users" do field(:name) @@ -10,6 +10,7 @@ defmodule Lucidboard.User do field(:avatar_url) embeds_one(:settings, UserSettings, on_replace: :delete) many_to_many(:cards_liked, Card, join_through: Like) + has_many(:board_roles, BoardRole) timestamps() end diff --git a/lib/lucidboard/seeds.ex b/lib/lucidboard/seeds.ex index f532c27..8a552d0 100644 --- a/lib/lucidboard/seeds.ex +++ b/lib/lucidboard/seeds.ex @@ -1,7 +1,8 @@ defmodule Lucidboard.Seeds do @moduledoc "Some database seed data" import Ecto.Query - alias Lucidboard.{Board, Card, Column, Like, Pile, Repo, User} + + alias Lucidboard.{Board, BoardRole, Card, Column, Like, Pile, Repo, User} def get_user do Repo.one(from(u in User, where: u.name == "bob")) || @@ -15,12 +16,13 @@ defmodule Lucidboard.Seeds do Repo.insert!(board2(user)) end - def board(user \\ nil) do + def board(user) do %User{id: uid} = user = user || get_user() Board.new( title: "My Test Board", user_id: user.id, + board_roles: [BoardRole.new(user_id: user.id, role: :owner)], columns: [ Column.new(title: "Col1", pos: 0), Column.new( diff --git a/lib/lucidboard/twiddler.ex b/lib/lucidboard/twiddler.ex index b929745..55840be 100644 --- a/lib/lucidboard/twiddler.ex +++ b/lib/lucidboard/twiddler.ex @@ -4,7 +4,7 @@ defmodule Lucidboard.Twiddler do """ import Ecto.Query alias Ecto.Changeset - alias Lucidboard.{Board, Event} + alias Lucidboard.{Account, Board, BoardRole, Event} alias Lucidboard.Repo alias Lucidboard.Twiddler.{Actions, Op} @@ -13,17 +13,23 @@ defmodule Lucidboard.Twiddler do @type action_ok_or_error :: {:ok, Board.t(), function, meta, Event.t()} | {:error, String.t()} - @spec act(Board.t(), action) :: action_ok_or_error - def act(%Board{} = board, {action_name, args}) when is_list(args) do - act(board, {action_name, Enum.into(args, %{})}) + @spec act(Board.t(), action, keyword) :: action_ok_or_error + def act(board, action, opts \\ []) + + def act(%Board{} = board, {action_name, args}, opts) when is_list(args) do + act(board, {action_name, Enum.into(args, %{})}, opts) end - def act(%Board{} = board, {action_name, args}) + def act(%Board{} = board, {action_name, args}, opts) when is_atom(action_name) and is_map(args) do - with true <- function_exported?(Actions, action_name, 2) || :no_action, - {:ok, _, _, _, _} = res <- apply(Actions, action_name, [board, args]) do + with true <- function_exported?(Actions, action_name, 3) || :no_action, + {:ok, _, _, _, _} = res <- + apply(Actions, action_name, [board, args, opts]) do res else + :unauthorized -> + {:ok, board, nil, nil, nil} + :noop -> {:ok, board, nil, nil, nil} @@ -43,12 +49,15 @@ defmodule Lucidboard.Twiddler do Repo.one( from(board in Board, where: board.id == ^id, + left_join: board_roles in assoc(board, :board_roles), + left_join: role_users in assoc(board_roles, :user), left_join: columns in assoc(board, :columns), left_join: piles in assoc(columns, :piles), left_join: cards in assoc(piles, :cards), left_join: likes in assoc(cards, :likes), preload: [ - columns: {columns, piles: {piles, cards: {cards, likes: likes}}} + columns: {columns, piles: {piles, cards: {cards, likes: likes}}}, + board_roles: {board_roles, user: role_users} ] ) ) @@ -97,9 +106,26 @@ defmodule Lucidboard.Twiddler do end @doc "Insert a board record" - @spec insert(Board.t() | Ecto.Changeset.t(Board.t())) :: - {:ok, Board.t()} | {:error, Ecto.Changeset.t(Board.t())} - def insert(%Board{} = board), do: Repo.insert(board) + @spec insert(Board.t() | Ecto.Changeset.t(Board.t()), User.t()) :: + {:ok, Board.t()} | {:error, any} + def insert(%Board{} = board, %{id: user_id} = _user) do + with {:ok, the_board} <- + Repo.transaction(fn -> create_board(board, user_id) end) do + {:ok, Repo.preload(the_board, :user)} + end + end + + # Creates 2 records: the Board and the BoardRole for the creator + defp create_board(board, user_id) do + {:ok, new_board} = Repo.insert(board) + + board_role = + BoardRole.new(user_id: user_id, board_id: new_board.id, role: :owner) + + :ok = Account.grant(new_board.id, board_role) + + new_board + end defp changeset_to_string(%Changeset{valid?: false, errors: errs}) do msg = diff --git a/lib/lucidboard/twiddler/actions.ex b/lib/lucidboard/twiddler/actions.ex index 5db4b40..d45072b 100644 --- a/lib/lucidboard/twiddler/actions.ex +++ b/lib/lucidboard/twiddler/actions.ex @@ -3,23 +3,25 @@ defmodule Lucidboard.Twiddler.Actions do Core logic responsible for handling different lucidboard changes. """ alias Ecto.Changeset - alias Lucidboard.{Board, Card, Column, Event} + alias Lucidboard.{Account, Board, BoardRole, Card, Column, Event} alias Lucidboard.Repo alias Lucidboard.Twiddler alias Lucidboard.Twiddler.{Glass, Op, QueryBuilder} import Ecto.Query - @spec update_board(Board.t(), map) :: Twiddler.action_ok_or_error() - def update_board(board, args) do - with %Changeset{valid?: true} = cs <- Board.changeset(board, args), + @spec update_board(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def update_board(board, args, opts) do + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + %Changeset{valid?: true} = cs <- Board.changeset(board, args), new_board <- Changeset.apply_changes(cs) do {:ok, new_board, fn -> Repo.update(cs) end, %{}, event("has updated the board settings.")} end end - @spec add_column(Board.t(), map) :: Twiddler.action_ok_or_error() - def add_column(board, args) do + @spec add_column(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def add_column(board, args, opts \\ []) do args = args |> Enum.into([]) @@ -27,7 +29,9 @@ defmodule Lucidboard.Twiddler.Actions do |> Column.new() |> Map.from_struct() - with %Changeset{valid?: true} = cs <- Column.changeset(%Column{}, args), + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + %Changeset{valid?: true} = cs <- Column.changeset(%Column{}, args), new_col <- Changeset.apply_changes(cs) do new_board = %{board | columns: List.insert_at(board.columns, -1, new_col)} @@ -36,9 +40,11 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec update_column(Board.t(), map) :: Twiddler.action_ok_or_error() - def update_column(board, args) do - with [id] <- grab(args, [:id]), + @spec update_column(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def update_column(board, args, opts \\ []) do + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + [id] <- grab(args, [:id]), {:ok, lens} <- Glass.column_by_id(board, id), %Changeset{valid?: true} = cs <- lens |> Focus.view(board) |> Column.changeset(args), @@ -50,8 +56,8 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec update_card(Board.t(), map) :: Twiddler.action_ok_or_error() - def update_card(board, args) do + @spec update_card(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def update_card(board, args, _opts \\ []) do with [id] <- grab(args, [:id]), {:ok, lens} <- Glass.card_by_id(board, id), %Changeset{valid?: true} = cs <- @@ -61,8 +67,8 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec delete_card(Board.t(), map) :: Twiddler.action_ok_or_error() - def delete_card(board, args) do + @spec delete_card(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def delete_card(board, args, _opts \\ []) do with [id] <- grab(args, [:id]), {:ok, card_path} <- Glass.card_path_by_id(board, id), {:ok, new_board, card, tx_fn} <- Op.cut_card(board, card_path) do @@ -71,7 +77,7 @@ defmodule Lucidboard.Twiddler.Actions do end end - def delete_column(board, args) do + def delete_column(board, args, _opts \\ []) do with [id] <- grab(args, [:id]), {:ok, lens} <- Glass.column_by_id(board, id) do column = Focus.view(lens, board) @@ -88,8 +94,9 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec add_and_lock_card(Board.t(), map) :: Twiddler.action_ok_or_error() - def add_and_lock_card(board, args) do + @spec add_and_lock_card(Board.t(), map, keyword) :: + Twiddler.action_ok_or_error() + def add_and_lock_card(board, args, _opts \\ []) do with [col_id, user_id] <- grab(args, [:col_id, :user_id]), {:ok, col_lens} <- Glass.column_by_id(board, col_id), {:ok, built_col, loaded_col, meta} <- @@ -100,11 +107,13 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec move_column(Board.t(), map) :: Twiddler.action_ok_or_error() - def move_column(board, args) do + @spec move_column(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def move_column(board, args, opts \\ []) do queryable = from(c in Column, where: c.board_id == ^board.id) - with [id, new_pos] <- grab(args, ~w/id pos/a), + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + [id, new_pos] <- grab(args, ~w/id pos/a), pos <- Enum.find(board.columns, fn c -> c.id == id end).pos, {:ok, col, new_cols} <- Op.move_item(board.columns, pos, new_pos), tx_fn <- QueryBuilder.move_item(queryable, id, pos, new_pos) do @@ -115,8 +124,10 @@ defmodule Lucidboard.Twiddler.Actions do end end - def move_column_up(board, args) do - with [id] <- grab(args, [:id]), + def move_column_up(board, args, opts \\ []) do + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + [id] <- grab(args, [:id]), {:ok, col} <- Op.column_by_id(board, id), true <- col.pos > 0 || :noop do pos = col.pos - 1 @@ -124,8 +135,10 @@ defmodule Lucidboard.Twiddler.Actions do end end - def move_column_down(board, args) do - with [id] <- grab(args, [:id]), + def move_column_down(board, args, opts \\ []) do + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + [id] <- grab(args, [:id]), {:ok, col} <- Op.column_by_id(board, id), true <- col.pos < length(board.columns) - 1 || :noop do pos = col.pos + 1 @@ -133,8 +146,9 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec move_pile_to_junction(Board.t(), map) :: Twiddler.action_ok_or_error() - def move_pile_to_junction(board, args) do + @spec move_pile_to_junction(Board.t(), map, keyword) :: + Twiddler.action_ok_or_error() + def move_pile_to_junction(board, args, _opts \\ []) do with [id, col_id, pos] <- grab(args, ~w/id col_id pos/a), {:ok, pile_path} <- Glass.pile_path_by_id(board, id), {:ok, dest_col_lens} <- Glass.column_by_id(board, col_id), @@ -147,7 +161,7 @@ defmodule Lucidboard.Twiddler.Actions do end # Moves a card to an empty space in a column, creating a new, 1-card pile - def move_card_to_junction(board, args) do + def move_card_to_junction(board, args, _opts \\ []) do with [id, col_id, pos] <- grab(args, ~w/id col_id pos/a), {:ok, card_path} <- Glass.card_path_by_id(board, id), {:ok, col_lens} <- Glass.column_by_id(board, col_id), @@ -159,7 +173,7 @@ defmodule Lucidboard.Twiddler.Actions do end end - def move_card_to_pile(board, args) do + def move_card_to_pile(board, args, _opts \\ []) do with [id, pile_id] <- grab(args, ~w/id pile_id/a), {:ok, card_path} <- Glass.card_path_by_id(board, id), {:ok, new_board, card, cut_fn} <- Op.cut_card(board, card_path), @@ -171,7 +185,7 @@ defmodule Lucidboard.Twiddler.Actions do end end - def flip_pile(board, args) do + def flip_pile(board, args, _opts \\ []) do with [id] <- grab(args, [:id]), {:ok, pile_lens} <- Glass.pile_by_id(board, id), {:ok, new_board, tx_fn} <- Op.flip_pile(board, pile_lens) do @@ -179,7 +193,7 @@ defmodule Lucidboard.Twiddler.Actions do end end - def unflip_pile(board, args) do + def unflip_pile(board, args, _opts \\ []) do with [id] <- grab(args, [:id]), {:ok, pile_lens} <- Glass.pile_by_id(board, id), {:ok, new_board, tx_fn} <- Op.unflip_pile(board, pile_lens) do @@ -188,7 +202,7 @@ defmodule Lucidboard.Twiddler.Actions do end @spec like(Board.t(), map) :: Twiddler.action_ok_or_error() - def like(board, args) do + def like(board, args, _opts \\ []) do with [id, user] <- grab(args, ~w/id user/a), {:ok, card_lens} <- Glass.card_by_id(board, id), card <- Focus.view(card_lens, board), @@ -201,8 +215,8 @@ defmodule Lucidboard.Twiddler.Actions do end end - @spec unlike(Board.t(), map) :: Twiddler.action_ok_or_error() - def unlike(board, args) do + @spec unlike(Board.t(), map, keyword) :: Twiddler.action_ok_or_error() + def unlike(board, args, _opts \\ []) do with [id, user] <- grab(args, ~w/id user/a), {:ok, card_lens} <- Glass.card_by_id(board, id) do card = Focus.view(card_lens, board) @@ -213,7 +227,7 @@ defmodule Lucidboard.Twiddler.Actions do end end - def sortby_likes(board, args) do + def sortby_likes(board, args, _opts \\ []) do with [id] <- grab(args, [:id]), {:ok, col_lens} <- Glass.column_by_id(board, id), column <- Focus.view(col_lens, board) do @@ -226,7 +240,41 @@ defmodule Lucidboard.Twiddler.Actions do new_board = Focus.set(col_lens, board, %{column | piles: sorted_piles}) {:ok, new_board, tx_fn, %{}, - event("Sorted `#{column.title}` column by likes.")} + event("sorted `#{column.title}` column by likes.")} + end + end + + # This action hits the database because we have to + def grant(board, args, opts \\ []) do + with true <- + Account.has_role?(Keyword.get(opts, :user), board) || :unauthorized, + [id, role] <- grab(args, [:id, :role]) do + board_role = + [user_id: id, board_id: board.id, role: role] + |> BoardRole.new() + |> Repo.preload(:user) + + {new_roles, revoke_tx_fn} = Op.revoke(id, board) + grant_tx_fn = fn -> :ok = Account.grant(board.id, board_role) end + + {:ok, %{board | board_roles: [board_role | new_roles]}, + [revoke_tx_fn, grant_tx_fn], %{}, + event( + "granted #{role} access to #{Account.display_name(board_role.user)}" + )} + end + end + + def revoke(board, args, opts \\ []) do + with [id] <- grab(args, [:id]), + user <- Keyword.get(opts, :user), + true <- Account.has_role?(user, board, :owner) || :unauthorized do + {new_roles, tx_fn} = Op.revoke(id, board) + board_role = Enum.find(board.board_roles, &(&1.user_id == id)) + display_name = board_role |> Map.get(:user) |> Account.display_name() + + {:ok, %{board | board_roles: new_roles}, tx_fn, %{}, + event("revoked #{board_role.role} access to #{display_name}")} end end diff --git a/lib/lucidboard/twiddler/op.ex b/lib/lucidboard/twiddler/op.ex index f3a4403..720edaf 100644 --- a/lib/lucidboard/twiddler/op.ex +++ b/lib/lucidboard/twiddler/op.ex @@ -8,7 +8,7 @@ defmodule Lucidboard.Twiddler.Op do """ import Ecto.Query alias Ecto.UUID - alias Lucidboard.{Board, Card, Column, Like, Pile, Repo, User} + alias Lucidboard.{Account, Board, Card, Column, Like, Pile, Repo, User} alias Lucidboard.LiveBoard.Scribe alias Lucidboard.Twiddler.Glass @@ -382,6 +382,12 @@ defmodule Lucidboard.Twiddler.Op do end) end + def revoke(user_id, board) do + tx_fn = fn -> :ok = Account.revoke(user_id, board.id) end + new_roles = Enum.reject(board.board_roles, &(&1.user_id == user_id)) + {new_roles, tx_fn} + end + defp renumber_positions(items) do items |> Enum.with_index() diff --git a/lib/lucidboard_web/controllers/board_controller.ex b/lib/lucidboard_web/controllers/board_controller.ex index 2ee34c4..5674278 100644 --- a/lib/lucidboard_web/controllers/board_controller.ex +++ b/lib/lucidboard_web/controllers/board_controller.ex @@ -40,7 +40,8 @@ defmodule LucidboardWeb.BoardController do board = Board.new(title: title, columns: columns, user: conn.assigns[:user]) - with {:ok, %Board{id: id} = board} <- Twiddler.insert(board) do + with {:ok, %Board{id: id} = board} <- + Twiddler.insert(board, conn.assigns.user) do Lucidboard.broadcast("short_boards", {:new, ShortBoard.from_board(board)}) {:see_other, Routes.board_path(conn, :index, id)} end diff --git a/lib/lucidboard_web/live/board_live.ex b/lib/lucidboard_web/live/board_live.ex index 6debbfe..ac92287 100644 --- a/lib/lucidboard_web/live/board_live.ex +++ b/lib/lucidboard_web/live/board_live.ex @@ -60,6 +60,7 @@ defmodule LucidboardWeb.BoardLive do |> assign(:delete_confirming_card_id, nil) |> assign(:online_count, online_count(board.id)) |> assign(:search, nil) + |> assign(:role_users_suggest, []) {:ok, socket} end @@ -237,6 +238,27 @@ defmodule LucidboardWeb.BoardLive do {:noreply, socket} end + def handle_event("role_suggest", %{"user" => input}, socket) do + suggestions = Account.suggest_users(input) + {:noreply, assign(socket, :role_users_suggest, suggestions)} + end + + def handle_event("grant", %{"user" => user_id}, socket) do + user = user_id |> String.to_integer() |> Account.get!() + live_board_action({:grant, id: user.id, role: :owner}, socket) + {:noreply, socket} + end + + def handle_event("revoke", user_id, socket) do + live_board_action({:revoke, id: String.to_integer(user_id)}, socket) + {:noreply, socket} + end + + def handle_event("sortby_votes", col_id, socket) do + live_board_action({:sortby_votes, id: col_id}, socket) + {:noreply, socket} + end + def handle_event("delete_column", col_id, socket) do live_board_action({:delete_column, id: col_id}, socket) {:noreply, socket} diff --git a/lib/lucidboard_web/live/dashboard_live.ex b/lib/lucidboard_web/live/dashboard_live.ex index 79ce22b..c0e1e30 100644 --- a/lib/lucidboard_web/live/dashboard_live.ex +++ b/lib/lucidboard_web/live/dashboard_live.ex @@ -28,7 +28,8 @@ defmodule LucidboardWeb.DashboardLive do assign(socket, short_boards: short_boards, board_pagination: board_pagination, - search_key: nil) + search_key: nil + ) {:ok, socket} end diff --git a/lib/lucidboard_web/templates/board/options.html.eex b/lib/lucidboard_web/templates/board/options.html.eex index f16189c..00829d6 100644 --- a/lib/lucidboard_web/templates/board/options.html.eex +++ b/lib/lucidboard_web/templates/board/options.html.eex @@ -30,6 +30,19 @@ else: {"Create", "Create Column"} %> +

<%= title %>

+ + <%= form_for @column_changeset, "#", [phx_submit: :column_save], fn f -> %> + <%= text_input f, :title, placeholder: "New column title", class: "input u-Mbm" %> + <%= error_tag f, :title %> + <%= submit button_txt, phx_disable_with: "Saving...", class: "button is-primary" %> + <% end %> + + +

Board Settings

@@ -59,4 +73,32 @@ <%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %> <% end %>
- + +
+

Manage Roles

+
+ Grant owner access to + + <%= for user <- @role_users_suggest do %> + + <% end %> + + +
+ +
+ \ No newline at end of file diff --git a/lib/lucidboard_web/templates/layout/header_user_menu.html.eex b/lib/lucidboard_web/templates/layout/header_user_menu.html.eex index d424e84..efd7170 100644 --- a/lib/lucidboard_web/templates/layout/header_user_menu.html.eex +++ b/lib/lucidboard_web/templates/layout/header_user_menu.html.eex @@ -1,11 +1,7 @@