From b0ddf99117fba15f27596048e72c2b5a039243a6 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 21 Jan 2026 08:02:38 +0100 Subject: [PATCH] Add admin authorization check for regenerate cycles button Restrict UI access to cycle regeneration to administrators only to prevent policy bypass via user interface --- .../show/membership_fees_component.ex | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index 186e8b7..5350e9f 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -554,45 +554,55 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do end def handle_event("regenerate_cycles", _params, socket) do - socket = assign(socket, :regenerating, true) - member = socket.assigns.member + actor = current_actor(socket) - case CycleGenerator.generate_cycles_for_member(member.id) do - {:ok, _new_cycles, _notifications} -> - # Reload member with cycles - actor = current_actor(socket) + # SECURITY: Only admins can manually regenerate cycles via UI + # Cycle generation itself uses system actor, but UI access should be restricted + if actor.role && actor.role.permission_set_name == "admin" do + socket = assign(socket, :regenerating, true) + member = socket.assigns.member - updated_member = - member - |> Ash.load!( - [ - :membership_fee_type, - membership_fee_cycles: [:membership_fee_type] - ], - actor: actor - ) + case CycleGenerator.generate_cycles_for_member(member.id) do + {:ok, _new_cycles, _notifications} -> + # Reload member with cycles + actor = current_actor(socket) - cycles = - Enum.sort_by( - updated_member.membership_fee_cycles || [], - & &1.cycle_start, - {:desc, Date} - ) + updated_member = + member + |> Ash.load!( + [ + :membership_fee_type, + membership_fee_cycles: [:membership_fee_type] + ], + actor: actor + ) - send(self(), {:member_updated, updated_member}) + cycles = + Enum.sort_by( + updated_member.membership_fee_cycles || [], + & &1.cycle_start, + {:desc, Date} + ) - {:noreply, - socket - |> assign(:member, updated_member) - |> assign(:cycles, cycles) - |> assign(:regenerating, false) - |> put_flash(:info, gettext("Cycles regenerated successfully"))} + send(self(), {:member_updated, updated_member}) - {:error, error} -> - {:noreply, - socket - |> assign(:regenerating, false) - |> put_flash(:error, format_error(error))} + {:noreply, + socket + |> assign(:member, updated_member) + |> assign(:cycles, cycles) + |> assign(:regenerating, false) + |> put_flash(:info, gettext("Cycles regenerated successfully"))} + + {:error, error} -> + {:noreply, + socket + |> assign(:regenerating, false) + |> put_flash(:error, format_error(error))} + end + else + {:noreply, + socket + |> put_flash(:error, gettext("Only administrators can regenerate cycles"))} end end