From ef1fba1aca13002b28d5a5ca9571f371c9b166fc Mon Sep 17 00:00:00 2001 From: max Date: Thu, 16 Apr 2026 21:53:53 +0200 Subject: [PATCH 1/3] Implement member role permissions and route guards --- config/dev.exs | 10 ++- lib/mailgun_logger/roles/roles.ex | 12 +++- lib/mailgun_logger/seeder.ex | 6 +- lib/mailgun_logger_web/plugs/authorize.ex | 28 ++++++++ lib/mailgun_logger_web/plugs/setup_check.ex | 6 +- lib/mailgun_logger_web/router.ex | 69 ++++++++++++++++--- .../templates/layout/app.html.heex | 47 +++++++++++-- 7 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 lib/mailgun_logger_web/plugs/authorize.ex diff --git a/config/dev.exs b/config/dev.exs index ce3c273..d4a672c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -4,15 +4,13 @@ port = System.get_env("PORT") || "7070" config :mailgun_logger, MailgunLoggerWeb.Endpoint, url: [ - host: System.get_env("HOST", "0.0.0.0"), - scheme: "https", + host: System.get_env("HOST", "localhost"), + scheme: "http", port: port ], - https: [ + http: [ + ip: {0, 0, 0, 0}, port: port, - cipher_suite: :strong, - keyfile: "priv/cert/selfsigned_key.pem", - certfile: "priv/cert/selfsigned.pem" ], live_reload: [ web_console_logger: true, diff --git a/lib/mailgun_logger/roles/roles.ex b/lib/mailgun_logger/roles/roles.ex index 8a873bb..3a2f8fb 100644 --- a/lib/mailgun_logger/roles/roles.ex +++ b/lib/mailgun_logger/roles/roles.ex @@ -5,6 +5,7 @@ defmodule MailgunLogger.Roles do alias MailgunLogger.User alias MailgunLogger.Repo + @member_role "member" @superuser_role "superuser" @admin_role "admin" @@ -12,7 +13,9 @@ defmodule MailgunLogger.Roles do @default_actions ~w() - @admin_actions ~w(do_stuff) ++ @default_actions + @member_actions ~w(view_events view_event_details edit_own_profile) ++ @default_actions + + @admin_actions ~w(view_stats manage_accounts manage_users) ++ @member_actions @superuser_actions ~w() ++ @admin_actions @@ -55,6 +58,11 @@ defmodule MailgunLogger.Roles do Enum.any?(roles, &can?(&1.name, action)) end + 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 @@ -67,6 +75,7 @@ defmodule MailgunLogger.Roles do def can?(_, _), do: false + def is?(%User{roles: roles}, :member), do: is(roles, "member") def is?(%User{roles: roles}, :superuser), do: is(roles, "superuser") def is?(%User{roles: roles}, :admin), do: is(roles, "admin") def is?(_, _), do: raise("Roles.is/2 requires roles to be preloaded") @@ -75,6 +84,7 @@ defmodule MailgunLogger.Roles do def abilities(%User{roles: []}), do: [] def abilities(%User{roles: roles}), do: hd(roles) |> abilities() + def abilities(%Role{name: "member"}), do: @member_actions def abilities(%Role{name: "admin"}), do: @admin_actions def abilities(%Role{name: "superuser"}), do: @superuser_actions diff --git a/lib/mailgun_logger/seeder.ex b/lib/mailgun_logger/seeder.ex index 6fe0588..44011f8 100644 --- a/lib/mailgun_logger/seeder.ex +++ b/lib/mailgun_logger/seeder.ex @@ -6,7 +6,11 @@ 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_web/plugs/authorize.ex b/lib/mailgun_logger_web/plugs/authorize.ex new file mode 100644 index 0000000..a69117b --- /dev/null +++ b/lib/mailgun_logger_web/plugs/authorize.ex @@ -0,0 +1,28 @@ +defmodule MailgunLoggerWeb.Plugs.Authorize do + import Plug.Conn + import Phoenix.Controller + + alias MailgunLogger.Roles + alias MailgunLoggerWeb.Router.Helpers, as: Routes + + @moduledoc """ + Plug that checks whether the current user has the required permission. + """ + + @spec init(atom()) :: atom() + def init(permission), do: permission + + @spec call(Plug.Conn.t(), atom()) :: Plug.Conn.t() + def call(conn, permission) do + user = conn.assigns.current_user + + if Roles.can?(user, permission) do + conn + else + conn + |> put_flash(:error, "You are not authorized to access this page.") + |> redirect(to: Routes.event_path(conn, :index)) + |> halt() + end + end +end diff --git a/lib/mailgun_logger_web/plugs/setup_check.ex b/lib/mailgun_logger_web/plugs/setup_check.ex index fa942c6..b008cbd 100644 --- a/lib/mailgun_logger_web/plugs/setup_check.ex +++ b/lib/mailgun_logger_web/plugs/setup_check.ex @@ -1,4 +1,5 @@ defmodule MailgunLoggerWeb.Plugs.SetupCheck do + import Plug.Conn import Phoenix.Controller alias MailgunLoggerWeb.Router.Helpers, as: Routes @@ -15,7 +16,10 @@ defmodule MailgunLoggerWeb.Plugs.SetupCheck do def call(conn, _) do case Users.any_users?() do true -> conn - false -> redirect(conn, to: Routes.setup_path(conn, :index)) + false -> + conn + |> redirect(to: Routes.setup_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..44a75c5 100644 --- a/lib/mailgun_logger_web/router.ex +++ b/lib/mailgun_logger_web/router.ex @@ -23,6 +23,30 @@ defmodule MailgunLoggerWeb.Router do plug(MailgunLoggerWeb.Plugs.Auth) end + pipeline :can_view_events do + plug(MailgunLoggerWeb.Plugs.Authorize, :view_events) + end + + pipeline :can_view_event_details do + plug(MailgunLoggerWeb.Plugs.Authorize, :view_event_details) + end + + pipeline :can_edit_own_profile do + plug(MailgunLoggerWeb.Plugs.Authorize, :edit_own_profile) + end + + pipeline :can_view_stats do + plug(MailgunLoggerWeb.Plugs.Authorize, :view_stats) + end + + pipeline :can_manage_accounts do + plug(MailgunLoggerWeb.Plugs.Authorize, :manage_accounts) + end + + pipeline :can_manage_users do + plug(MailgunLoggerWeb.Plugs.Authorize, :manage_users) + end + # Always except in prod if Application.compile_env(:mailgun_logger, :env) == :dev do forward("/sent_emails", Bamboo.SentEmailViewerPlug) @@ -66,25 +90,54 @@ defmodule MailgunLoggerWeb.Router do scope "/setup", MailgunLoggerWeb do pipe_through(:browser) get("/", SetupController, :index) + get("/non-affiliation", PageController, :non_affiliation) post("/", SetupController, :create_root) end scope "/", MailgunLoggerWeb do - pipe_through([:browser, :auth]) + pipe_through([:browser, :auth, :can_view_events]) - get("/logout", AuthController, :logout) + get("/", PageController, :index) + get("/events", EventController, :index) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :can_view_event_details]) - resources("/events", EventController, only: [:index, :show]) + get("/events/:id", EventController, :show) get("/events/:id/stored_message", EventController, :stored_message) - resources("/accounts", AccountController, except: [:show]) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :can_edit_own_profile]) + get("/profile", ProfileController, :edit) put("/profile", ProfileController, :update) - resources("/users", UserController, except: [:show]) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth]) + + get("/logout", AuthController, :logout) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :can_view_stats]) - get("/", PageController, :index) - post("/trigger-run", PageController, :trigger_run) get("/stats", PageController, :stats) get("/graphs", PageController, :graphs) - get("/non-affiliation", PageController, :non_affiliation) + post("/trigger-run", PageController, :trigger_run) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :can_manage_accounts]) + + resources("/accounts", AccountController, except: [:show]) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :can_manage_users]) + + resources("/users", UserController, except: [:show]) 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..fda2b06 100644 --- a/lib/mailgun_logger_web/templates/layout/app.html.heex +++ b/lib/mailgun_logger_web/templates/layout/app.html.heex @@ -15,7 +15,8 @@ var html = document.documentElement; var theme = html.getAttribute("data-theme"); if (theme === "system") { - html.setAttribute("data-theme", + html.setAttribute( + "data-theme", window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark" ); } @@ -38,19 +39,51 @@