diff --git a/.gitignore b/.gitignore index a155b1a..06bd394 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ docker-compose.yml node_modules .serena .playwright-mcp + +.DS_Store + diff --git a/lib/mailgun_logger/roles/roles.ex b/lib/mailgun_logger/roles/roles.ex index 8a873bb..16273b0 100644 --- a/lib/mailgun_logger/roles/roles.ex +++ b/lib/mailgun_logger/roles/roles.ex @@ -7,12 +7,18 @@ defmodule MailgunLogger.Roles do @superuser_role "superuser" @admin_role "admin" + # role met limited rechten + @member_role "member" ######################################################### @default_actions ~w() - @admin_actions ~w(do_stuff) ++ @default_actions + #member kan enkel events checken + #w() maakt lijst van woorden splitsend op whitespace + @member_actions ~w(view_events) + + @admin_actions ~w(do_stuff) ++ @member_actions ++ @default_actions @superuser_actions ~w() ++ @admin_actions @@ -55,6 +61,12 @@ defmodule MailgunLogger.Roles do Enum.any?(roles, &can?(&1.name, action)) end + # rechten member rol + for action <- @member_actions do + action = String.to_atom(action) + def can?(@member_role, unquote(action)), do: true + end + for action <- @admin_actions do action = String.to_atom(action) def can?(@admin_role, unquote(action)), do: true @@ -69,6 +81,8 @@ defmodule MailgunLogger.Roles do def is?(%User{roles: roles}, :superuser), do: is(roles, "superuser") def is?(%User{roles: roles}, :admin), do: is(roles, "admin") + # check user rol + def is?(%User{roles: roles}, :member), do: is(roles, "member") def is?(_, _), do: raise("Roles.is/2 requires roles to be preloaded") defp is(roles, role) when is_binary(role), do: Enum.map(roles, & &1.name) |> Enum.member?(role) @@ -77,6 +91,8 @@ defmodule MailgunLogger.Roles do def abilities(%User{roles: roles}), do: hd(roles) |> abilities() def abilities(%Role{name: "admin"}), do: @admin_actions def abilities(%Role{name: "superuser"}), do: @superuser_actions + # mogelijkheden vr member + def abilities(%Role{name: "member"}), do: @member_actions def roles(%User{roles: roles}), do: Enum.map(roles, & &1.name) end diff --git a/lib/mailgun_logger/seeder.ex b/lib/mailgun_logger/seeder.ex index 6fe0588..c91360d 100644 --- a/lib/mailgun_logger/seeder.ex +++ b/lib/mailgun_logger/seeder.ex @@ -6,7 +6,8 @@ defmodule MailgunLogger.Seeder do # alias MailgunLogger.UserRole - @roles [%Role{name: "superuser"}, %Role{name: "admin"}] + @roles [%Role{name: "superuser"}, %Role{name: "admin"}, %Role{name: "member"}] + def run do Enum.each(@roles, &insert_if_new(&1)) diff --git a/lib/mailgun_logger/users/user.ex b/lib/mailgun_logger/users/user.ex index 9ed75cf..3000635 100644 --- a/lib/mailgun_logger/users/user.ex +++ b/lib/mailgun_logger/users/user.ex @@ -109,6 +109,18 @@ defmodule MailgunLogger.User do put_change(changeset, :token, token) end + @doc """ + voeg rollen toe aan changeset + + bestaande rollen worden overwritten + + @spec = params en :: return type +""" + @spec with_roles(Ecto.Changeset.t(), [Role.t()]) :: Ecto.Changeset.t() + def with_roles(%Ecto.Changeset{} = changeset, roles) do + put_assoc(changeset, :roles, roles) + end + # putassoc => associatie zette in changeset + @doc false def full_name(nil), do: "" def full_name(%User{lastname: nil, firstname: nil, email: nil}), do: "" diff --git a/lib/mailgun_logger/users/users.ex b/lib/mailgun_logger/users/users.ex index ae6902e..dbc37de 100644 --- a/lib/mailgun_logger/users/users.ex +++ b/lib/mailgun_logger/users/users.ex @@ -3,6 +3,7 @@ defmodule MailgunLogger.Users do alias MailgunLogger.Repo alias MailgunLogger.User + alias MailgunLogger.Roles # nodig voor rollen ophalen bij create/update user with roles @type ecto_user() :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @@ -158,6 +159,28 @@ defmodule MailgunLogger.Users do |> Repo.update() end + # maak nieuwe User aan met gegeven rollen + @spec create_user_with_roles(map(), [integer()]) :: ecto_user() + def create_user_with_roles(params, role_ids) do + roles = Roles.get_roles_by_id(role_ids) + + %User{} + |> User.changeset(params) + |> User.with_roles(roles) + |> Repo.insert() + end + + # update user en overschrijft rollen + @spec update_user_with_roles(User.t(), map(), [integer()]) :: ecto_user() + def update_user_with_roles(user, params, role_ids) do + roles = Roles.get_roles_by_id(role_ids) + + user + |> User.update_changeset(params) + |> User.with_roles(roles) + |> Repo.update() + end + @spec delete_user(User.t()) :: ecto_user() def delete_user(%User{} = user) do Repo.delete(user) diff --git a/lib/mailgun_logger_web/controllers/user_controller.ex b/lib/mailgun_logger_web/controllers/user_controller.ex index 1128c69..44f9aa2 100644 --- a/lib/mailgun_logger_web/controllers/user_controller.ex +++ b/lib/mailgun_logger_web/controllers/user_controller.ex @@ -3,6 +3,7 @@ defmodule MailgunLoggerWeb.UserController do alias MailgunLogger.Users alias MailgunLogger.User + alias MailgunLogger.Roles def index(conn, _) do users = Users.list_users() @@ -11,42 +12,88 @@ defmodule MailgunLoggerWeb.UserController do def new(conn, _) do changeset = User.changeset(%User{}) - render(conn, :new, changeset: changeset) + # laad alle beschikbare rollen voor het formulier + roles = Roles.list_roles() + render(conn, :new, changeset: changeset, roles: roles, selected_role_ids: []) end def create(conn, %{"user" => params}) do - case Users.create_user(params) do - {:ok, _} -> redirect(conn, to: Routes.user_path(conn, :index)) - {:error, changeset} -> render(conn, :new, changeset: changeset) + role_ids = Map.get(params, "role_ids", []) + user_params = Map.delete(params, "role_ids") + + case Users.create_user_with_roles(user_params, role_ids) do + {:ok, _} -> + redirect(conn, to: Routes.user_path(conn, :index)) + + {:error, changeset} -> + roles = Roles.list_roles() + render(conn, :new, changeset: changeset, roles: roles, selected_role_ids: role_ids) end end def edit(conn, %{"id" => id}) do user = Users.get_user!(id) changeset = User.changeset(user) - render(conn, :edit, changeset: changeset, user: user) + # laad alle rollen en de huidige rollen van de gebruiker + roles = Roles.list_roles() + selected_role_ids = Enum.map(user.roles, & &1.id) + render(conn, :edit, changeset: changeset, user: user, roles: roles, selected_role_ids: selected_role_ids) end def update(conn, %{"id" => id, "user" => params}) do + current_user = conn.assigns.current_user user = Users.get_user!(id) + role_ids = Map.get(params, "role_ids", []) + user_params = Map.delete(params, "role_ids") - case Users.update_user(user, params) do - {:ok, _} -> - redirect(conn, to: Routes.user_path(conn, :index)) + # prevent dat user zijn eigen downgrade + if self_downgrade?(current_user, user, role_ids) do + changeset = User.changeset(user) + roles = Roles.list_roles() + selected_role_ids = Enum.map(user.roles, & &1.id) - {:error, changeset} -> - render(conn, :edit, changeset: changeset, user: user) + conn + |> put_flash(:error, "Je kan je eigen rol niet verlagen.") + |> render(:edit, changeset: changeset, user: user, roles: roles, selected_role_ids: selected_role_ids) + else + case Users.update_user_with_roles(user, user_params, role_ids) do + {:ok, _} -> + redirect(conn, to: Routes.user_path(conn, :index)) + + {:error, changeset} -> + roles = Roles.list_roles() + render(conn, :edit, changeset: changeset, user: user, roles: roles, selected_role_ids: role_ids) + end end end def delete(conn, %{"id" => id}) do - {:ok, _} = - id - |> Users.get_user!() - |> Users.delete_user() + user = Users.get_user!(id) + + # Verwijder eerst de rollen voor we de user deleten + {:ok, user} = Users.update_user_with_roles(user, %{}, []) + {:ok, _} = Users.delete_user(user) conn |> put_flash(:info, "user deleted successfully.") |> redirect(to: Routes.user_path(conn, :index)) end + + # check od user zijn eigen wilt downgrade + defp self_downgrade?(current_user, user, new_role_ids) do + if current_user.id == user.id do + elevated_roles = Enum.filter(user.roles, &(&1.name in ["admin", "superuser"])) + elevated_role_ids = Enum.map(elevated_roles, & &1.id) + new_role_ids_int = Enum.map(new_role_ids, &String.to_integer/1) + + + # true als er een verhoogde rol is die niet meer in de nieuwe rollen zit + Enum.any?(elevated_role_ids, &(&1 not in new_role_ids_int)) + else + + false + + end + + end end diff --git a/lib/mailgun_logger_web/plugs/require_permission.ex b/lib/mailgun_logger_web/plugs/require_permission.ex new file mode 100644 index 0000000..2020247 --- /dev/null +++ b/lib/mailgun_logger_web/plugs/require_permission.ex @@ -0,0 +1,30 @@ +defmodule MailgunLoggerWeb.Plugs.RequirePermission do + import Plug.Conn + import Phoenix.Controller + alias MailgunLogger.Roles + alias MailgunLoggerWeb.Router.Helpers, as: Routes + + + @moduledoc """ + plug die checkt of huidige gebruiker de vereiste permission heeft + if false wordt gebruiker geredirect naar events page + reminder: plug (elixir) = middleware + """ + + def init(action), do: action + def call(conn, action) do + user = conn.assigns[:current_user] + + if user && Roles.can?(user, action) do + # user heeft toestemming, ga verder + conn + else + # geen permi -> redirect naar events + conn + |> put_flash(:error, "je hebt geen permissie tot deze pagina") + |> redirect(to: Routes.event_path(conn, :index)) + |> halt() + end + + end +end diff --git a/lib/mailgun_logger_web/router.ex b/lib/mailgun_logger_web/router.ex index 7297982..d010d0b 100644 --- a/lib/mailgun_logger_web/router.ex +++ b/lib/mailgun_logger_web/router.ex @@ -23,6 +23,17 @@ defmodule MailgunLoggerWeb.Router do plug(MailgunLoggerWeb.Plugs.Auth) end + # access voor member en hoger (events bekijke) + pipeline :require_events_access do + plug(MailgunLoggerWeb.Plugs.RequirePermission, :view_events) + end + + + # access alleen voor admin en superuser + pipeline :require_admin_access do + plug(MailgunLoggerWeb.Plugs.RequirePermission, :do_stuff) + end + # Always except in prod if Application.compile_env(:mailgun_logger, :env) == :dev do forward("/sent_emails", Bamboo.SentEmailViewerPlug) @@ -69,22 +80,34 @@ defmodule MailgunLoggerWeb.Router do post("/", SetupController, :create_root) end + # logout is toegankelijk voor alle users + # hoeft geen check voor :require_events_access scope "/", MailgunLoggerWeb do pipe_through([:browser, :auth]) get("/logout", AuthController, :logout) + end + + # accesible voor member en boven + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :require_events_access]) + get("/", PageController, :index) + get("/non-affiliation", PageController, :non_affiliation) resources("/events", EventController, only: [:index, :show]) get("/events/:id/stored_message", EventController, :stored_message) - resources("/accounts", AccountController, except: [:show]) get("/profile", ProfileController, :edit) put("/profile", ProfileController, :update) - resources("/users", UserController, except: [:show]) + end - get("/", PageController, :index) + # alleen toegankelijk voor superuser en amdin + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :require_admin_access]) + + resources("/accounts", AccountController, except: [:show]) + resources("/users", UserController, except: [:show]) post("/trigger-run", PageController, :trigger_run) get("/stats", PageController, :stats) get("/graphs", PageController, :graphs) - get("/non-affiliation", PageController, :non_affiliation) end end diff --git a/lib/mailgun_logger_web/templates/layout/app.html.heex b/lib/mailgun_logger_web/templates/layout/app.html.heex index 6a78307..b93efbb 100644 --- a/lib/mailgun_logger_web/templates/layout/app.html.heex +++ b/lib/mailgun_logger_web/templates/layout/app.html.heex @@ -39,9 +39,16 @@