CRITICAL: - Membership: remove :role/:org_id/:user_id from mass-assignment cast; use explicit create_changeset/4 and role_update_changeset/2 - GraphQL member resolver: add authorize_invite/3 checking admin/owner role and role hierarchy - REST invitations controller: add auth check for invite creation HIGH: - ApiToken: remove :organization_id/:user_id from cast; use explicit create_changeset/4 MEDIUM: - Move 8 LiveView Ecto queries into context modules (Admin, Alerts, Coverages, OnCall, Snmp) - Replace Process.put/Process.get with socket assigns for unresolved_alert_count (user_auth + layouts + 50 templates) - Add batch get_utilization_for_interfaces/1 to eliminate N+1 capacity queries in device show - Replace Process.sleep with Process.monitor/assert_receive or Process.send_after in 5 test files LOW: - Add handle_params/3 to UserResetPasswordLive, UserRegistrationLive, StatusPageLive - Remove redundant Repo.preload calls; add preloads to list_site_devices/1 - Fix @impl annotations and credo nesting warnings
261 lines
8.1 KiB
Elixir
261 lines
8.1 KiB
Elixir
defmodule ToweropsWeb.Admin.OrgLive.Index do
|
|
@moduledoc """
|
|
Admin interface for viewing and managing organizations.
|
|
"""
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Admin
|
|
alias Towerops.Billing
|
|
alias Towerops.Organizations.Organization
|
|
|
|
on_mount {ToweropsWeb.UserAuth, :require_superuser}
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
orgs = Admin.list_all_organizations()
|
|
default_free_devices = Billing.default_free_devices()
|
|
default_price = Billing.default_price_per_device()
|
|
active_sub_count = Admin.count_active_subscriptions()
|
|
|
|
ip =
|
|
case get_connect_info(socket, :peer_data) do
|
|
%{address: address} -> to_string(:inet_parse.ntoa(address))
|
|
_ -> "unknown"
|
|
end
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, t("All Organizations"))
|
|
|> assign(:timezone, socket.assigns.current_scope.timezone)
|
|
|> assign(:organizations, orgs)
|
|
|> assign(:client_ip, ip)
|
|
|> assign(:editing_org, nil)
|
|
|> assign(:override_form, nil)
|
|
|> assign(:default_free_devices, default_free_devices)
|
|
|> assign(:default_price, default_price)
|
|
|> assign(:active_sub_count, active_sub_count)
|
|
|> assign(:editing_global, false)
|
|
|> assign(:global_form, nil)
|
|
|> assign(:show_confirm, false)
|
|
|> assign(:pending_pricing_params, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(%{"edit_global" => "true"} = _params, _url, socket) do
|
|
form =
|
|
to_form(%{
|
|
"default_free_devices" => socket.assigns.default_free_devices,
|
|
"default_price_per_device" => Decimal.to_string(socket.assigns.default_price)
|
|
})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:editing_global, true)
|
|
|> assign(:global_form, form)
|
|
|> assign(:show_confirm, false)
|
|
|> assign(:editing_org, nil)
|
|
|> assign(:override_form, nil)}
|
|
end
|
|
|
|
def handle_params(params, _url, socket) do
|
|
case params["edit"] do
|
|
nil ->
|
|
{:noreply,
|
|
socket
|
|
|> assign(:editing_org, nil)
|
|
|> assign(:override_form, nil)
|
|
|> assign(:editing_global, false)}
|
|
|
|
org_id ->
|
|
org = Enum.find(socket.assigns.organizations, &(&1.id == org_id))
|
|
|
|
if org do
|
|
changeset = Organization.billing_override_changeset(org, %{})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:editing_org, org)
|
|
|> assign(:override_form, to_form(changeset))
|
|
|> assign(:editing_global, false)}
|
|
else
|
|
{:noreply,
|
|
socket
|
|
|> assign(:editing_org, nil)
|
|
|> assign(:override_form, nil)
|
|
|> assign(:editing_global, false)}
|
|
end
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("edit_overrides", %{"id" => org_id}, socket) do
|
|
{:noreply, push_patch(socket, to: ~p"/admin/organizations?edit=#{org_id}")}
|
|
end
|
|
|
|
def handle_event("validate_overrides", %{"organization" => params}, socket) do
|
|
changeset =
|
|
socket.assigns.editing_org
|
|
|> Organization.billing_override_changeset(params)
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, :override_form, to_form(changeset))}
|
|
end
|
|
|
|
def handle_event("save_overrides", %{"organization" => params}, socket) do
|
|
superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user
|
|
org = socket.assigns.editing_org
|
|
|
|
case Admin.update_billing_overrides(org.id, params, superuser.id, socket.assigns.client_ip) do
|
|
{:ok, _updated_org} ->
|
|
orgs = Admin.list_all_organizations()
|
|
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t_admin("Billing overrides updated"))
|
|
|> assign(:organizations, orgs)
|
|
|> push_patch(to: ~p"/admin/organizations")}
|
|
|
|
{:error, changeset} ->
|
|
{:noreply, assign(socket, :override_form, to_form(changeset))}
|
|
end
|
|
end
|
|
|
|
def handle_event("clear_overrides", _params, socket) do
|
|
superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user
|
|
org = socket.assigns.editing_org
|
|
|
|
attrs = %{custom_free_device_limit: nil, custom_price_per_device: nil}
|
|
|
|
case Admin.update_billing_overrides(org.id, attrs, superuser.id, socket.assigns.client_ip) do
|
|
{:ok, _updated_org} ->
|
|
orgs = Admin.list_all_organizations()
|
|
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t_admin("Billing overrides cleared"))
|
|
|> assign(:organizations, orgs)
|
|
|> push_patch(to: ~p"/admin/organizations")}
|
|
|
|
{:error, _changeset} ->
|
|
{:noreply, put_flash(socket, :error, t_admin("Failed to clear overrides"))}
|
|
end
|
|
end
|
|
|
|
def handle_event("cancel_overrides", _params, socket) do
|
|
{:noreply, push_patch(socket, to: ~p"/admin/organizations")}
|
|
end
|
|
|
|
def handle_event("delete_org", %{"id" => org_id}, socket) do
|
|
superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user
|
|
ip = socket.assigns.client_ip
|
|
|
|
case Admin.delete_organization(org_id, superuser.id, ip) do
|
|
{:ok, _} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t_admin("Organization deleted successfully"))
|
|
|> assign(:organizations, Admin.list_all_organizations())}
|
|
|
|
{:error, _} ->
|
|
{:noreply, put_flash(socket, :error, t_admin("Failed to delete organization"))}
|
|
end
|
|
end
|
|
|
|
def handle_event("edit_global_defaults", _params, socket) do
|
|
{:noreply, push_patch(socket, to: ~p"/admin/organizations?edit_global=true")}
|
|
end
|
|
|
|
def handle_event("validate_global_pricing", %{"global_pricing" => params}, socket) do
|
|
errors = validate_global_pricing_params(params)
|
|
form = to_form(params, errors: errors)
|
|
|
|
{:noreply, assign(socket, :global_form, form)}
|
|
end
|
|
|
|
def handle_event("save_global_pricing", %{"global_pricing" => params}, socket) do
|
|
case validate_global_pricing_params(params) do
|
|
[] ->
|
|
# Valid - show confirmation
|
|
{:noreply,
|
|
socket
|
|
|> assign(:show_confirm, true)
|
|
|> assign(:pending_pricing_params, params)}
|
|
|
|
errors ->
|
|
form = to_form(params, errors: errors)
|
|
{:noreply, assign(socket, :global_form, form)}
|
|
end
|
|
end
|
|
|
|
def handle_event("confirm_pricing_update", _params, socket) do
|
|
params = socket.assigns.pending_pricing_params
|
|
superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user
|
|
|
|
case Admin.update_global_pricing(params, superuser.id, socket.assigns.client_ip) do
|
|
{:ok, result} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(
|
|
:info,
|
|
"Pricing updated successfully. #{result.succeeded}/#{result.total} subscriptions migrated."
|
|
)
|
|
|> push_patch(to: ~p"/admin/organizations")}
|
|
|
|
{:error, reason} ->
|
|
{:noreply, put_flash(socket, :error, "Failed to update pricing: #{inspect(reason)}")}
|
|
end
|
|
end
|
|
|
|
def handle_event("cancel_global_edit", _params, socket) do
|
|
{:noreply, push_patch(socket, to: ~p"/admin/organizations")}
|
|
end
|
|
|
|
defp validate_global_pricing_params(params) do
|
|
[]
|
|
|> validate_free_devices(params)
|
|
|> validate_price_per_device(params)
|
|
end
|
|
|
|
defp validate_free_devices(errors, params) do
|
|
case Map.get(params, "default_free_devices") do
|
|
nil -> errors
|
|
value -> validate_free_devices_value(errors, value)
|
|
end
|
|
end
|
|
|
|
defp validate_free_devices_value(errors, value) do
|
|
case Integer.parse(value) do
|
|
{int, _} when int > 0 and int < 10_000 ->
|
|
errors
|
|
|
|
_ ->
|
|
[{:default_free_devices, {"must be greater than 0 and less than 10,000", []}} | errors]
|
|
end
|
|
end
|
|
|
|
defp validate_price_per_device(errors, params) do
|
|
case Map.get(params, "default_price_per_device") do
|
|
nil -> errors
|
|
value -> validate_price_per_device_value(errors, value)
|
|
end
|
|
end
|
|
|
|
defp validate_price_per_device_value(errors, value) do
|
|
case Decimal.parse(value) do
|
|
{decimal, _} -> validate_price_range(errors, decimal)
|
|
:error -> [{:default_price_per_device, {"must be a valid decimal", []}} | errors]
|
|
end
|
|
end
|
|
|
|
defp validate_price_range(errors, decimal) do
|
|
valid_range? =
|
|
Decimal.compare(decimal, "0.01") in [:gt, :eq] and
|
|
Decimal.compare(decimal, "999.99") in [:lt, :eq]
|
|
|
|
if valid_range? do
|
|
errors
|
|
else
|
|
[{:default_price_per_device, {"must be between 0.01 and 999.99", []}} | errors]
|
|
end
|
|
end
|
|
end
|