towerops/lib/towerops_web/live/helpers/access_control.ex
Graham McIntire c4e301a84c fix: address code review findings - memory leaks and redundant queries (#192)
Fixed 5 critical issues from code review:

1. **GlobalSearchTrigger hook listener leak**
   - Added destroyed() callback to remove click listener
   - Prevents memory leak on LiveView unmount

2. **NetworkMap/WeathermapViewer hook listener leaks**
   - Store zoom/fit button handlers as properties
   - Remove DOM event listeners in destroyed() callback
   - Fixes leak on every LiveView remount

3. **SitesMap uses undocumented window.liveSocket.execJS**
   - Changed to proper LiveView pattern using pushEvent
   - Attach listener via Leaflet's popupopen event

4. **handle_info(:refresh_data) double DB fetch**
   - Removed redundant Devices.get_device call
   - Reuse device from assign_base_data

5. **Redundant DB queries in DeviceLive.Show overview**
   - Changed load_sensor_chart_data to accept snmp_device
   - Changed load_overall_traffic_chart_data to accept snmp_device
   - Eliminated 4 redundant Snmp.get_device_with_associations calls

Also removed unused functions:
- load_equipment_data/2
- reload_active_tab_data/1
- assign_subscriber_impact/1

Note: DeviceLive.Show module size (2252 lines) is acknowledged but
deferred as it requires larger architectural refactoring.

Reviewed-on: graham/towerops-web#192
2026-03-27 16:34:42 -05:00

112 lines
3 KiB
Elixir

defmodule ToweropsWeb.Live.Helpers.AccessControl do
@moduledoc """
Centralized organization-based access control for LiveViews.
Provides functions to verify that users have access to resources
(devices, sites, alerts) within their organization scope.
"""
alias Towerops.Alerts.Alert
alias Towerops.Devices
alias Towerops.Repo
alias Towerops.Sites
@doc """
Verifies that a device belongs to the specified organization.
Returns `{:ok, device}` if access is granted, or an error tuple.
## Examples
iex> verify_device_access(device_id, org_id)
{:ok, %Device{}}
iex> verify_device_access(device_id, wrong_org_id)
{:error, :unauthorized}
iex> verify_device_access("nonexistent", org_id)
{:error, :not_found}
"""
@spec verify_device_access(binary(), binary()) ::
{:ok, Devices.Device.t()} | {:error, :not_found | :unauthorized}
def verify_device_access(device_id, organization_id) do
case Devices.get_device(device_id) do
nil ->
{:error, :not_found}
device ->
if device.organization_id == organization_id do
{:ok, device}
else
{:error, :unauthorized}
end
end
end
@doc """
Verifies that a site belongs to the specified organization.
Returns `{:ok, site}` if access is granted, or an error tuple.
## Examples
iex> verify_site_access(site_id, org_id)
{:ok, %Site{}}
iex> verify_site_access(site_id, wrong_org_id)
{:error, :unauthorized}
iex> verify_site_access("nonexistent", org_id)
{:error, :not_found}
"""
@spec verify_site_access(binary(), binary()) ::
{:ok, Sites.Site.t()} | {:error, :not_found | :unauthorized}
def verify_site_access(site_id, organization_id) do
case Sites.get_site(site_id) do
nil ->
{:error, :not_found}
site ->
if site.organization_id == organization_id do
{:ok, site}
else
{:error, :unauthorized}
end
end
end
@doc """
Verifies that an alert's device belongs to the specified organization.
Returns `{:ok, alert}` if access is granted, or an error tuple.
The alert is preloaded with `device: [site: :organization]` for access checking.
## Examples
iex> verify_alert_access(alert_id, org_id)
{:ok, %Alert{device: %Device{site: %Site{organization: %Organization{}}}}}
iex> verify_alert_access(alert_id, wrong_org_id)
{:error, :unauthorized}
iex> verify_alert_access("nonexistent", org_id)
{:error, :not_found}
"""
@spec verify_alert_access(binary(), binary()) ::
{:ok, Alert.t()} | {:error, :not_found | :unauthorized}
def verify_alert_access(alert_id, organization_id) do
case Repo.get(Alert, alert_id) do
nil ->
{:error, :not_found}
alert ->
alert = Repo.preload(alert, [:acknowledged_by, device: [site: :organization]])
if alert.device.site.organization_id == organization_id do
{:ok, alert}
else
{:error, :unauthorized}
end
end
end
end