defmodule ToweropsWeb.Api.AccountDataController do @moduledoc """ API controller for GDPR Right to Access - allows users to download all their data in JSON format """ use ToweropsWeb, :controller alias Towerops.Admin alias Towerops.Admin.AuditLogger alias Towerops.Alerts alias Towerops.Devices alias Towerops.Organizations @doc """ Returns all user data in JSON format (GDPR Right to Access compliance) """ def show(conn, _params) do user = conn.assigns.current_scope.user # Verify user account is confirmed before allowing data export if user.confirmed_at do # Log the data export for audit trail AuditLogger.log_user_data_exported(conn, user.id) # Gather all user data data = %{ profile: build_profile_data(user), organizations: build_organizations_data(user), devices: build_devices_data(user), alerts: build_alerts_data(user), audit_logs: build_audit_logs_data(user), export_info: %{ exported_at: DateTime.utc_now(), format: "JSON", gdpr_article: "Article 15 - Right to Access" } } conn |> put_resp_content_type("application/json") |> put_resp_header( "content-disposition", "attachment; filename=\"towerops-data-#{user.id}-#{DateTime.to_unix(DateTime.utc_now())}.json\"" ) |> json(data) else conn |> put_status(:forbidden) |> json(%{error: "Email address must be confirmed before exporting account data"}) |> halt() end end # Build profile data defp build_profile_data(user) do %{ id: user.id, email: user.email, first_name: user.first_name, last_name: user.last_name, timezone: user.timezone, is_superuser: user.is_superuser, confirmed_at: user.confirmed_at, inserted_at: user.inserted_at, updated_at: user.updated_at } end # Build organizations data defp build_organizations_data(user) do user.id |> Organizations.list_user_organizations() |> Enum.map(fn org -> # Organization has the user's membership preloaded. Handle the # zero-or-more-than-one cases defensively — historically this was a # plain `[membership] = ...` pattern match that crashed on any # unexpected count and produced a 500 from this user-facing endpoint. role = case org.memberships do [%{role: :owner} | _] -> "owner" [_first | _] -> "member" _ -> "member" end %{ id: org.id, name: org.name, slug: org.slug, role: role, inserted_at: org.inserted_at, updated_at: org.updated_at } end) end # Build devices data defp build_devices_data(user) do org_ids = user.id |> Organizations.list_user_organizations() |> Enum.map(& &1.id) if Enum.empty?(org_ids) do [] else org_ids |> Devices.list_devices_for_organizations() |> Enum.map(fn device -> %{ id: device.id, name: device.name, ip_address: device.ip_address, snmp_community: "[REDACTED]", monitoring_enabled: device.monitoring_enabled, snmp_enabled: device.snmp_enabled, status: device.status, organization: %{ id: device.organization_id, name: device.organization.name }, site: %{ id: device.site_id, name: device.site.name }, inserted_at: device.inserted_at, updated_at: device.updated_at } end) end end # Build alerts data (last 90 days) defp build_alerts_data(user) do org_ids = user.id |> Organizations.list_user_organizations() |> Enum.map(& &1.id) if Enum.empty?(org_ids) do [] else ninety_days_ago = DateTime.add(DateTime.utc_now(), -90, :day) org_ids |> Alerts.list_alerts_for_organizations(since: ninety_days_ago) |> Enum.map(fn alert -> %{ id: alert.id, alert_type: alert.alert_type, message: alert.message, triggered_at: alert.triggered_at, resolved_at: alert.resolved_at, acknowledged_at: alert.acknowledged_at, device: %{ id: alert.device_id, name: alert.device.name }, inserted_at: alert.inserted_at } end) end end # Build audit logs data defp build_audit_logs_data(user) do user.id |> Admin.list_audit_logs_for_user(limit: 1000) |> Enum.map(fn log -> %{ id: log.id, action: log.action, actor_email: if(log.superuser, do: log.superuser.email), target_user_id: log.target_user_id, metadata: log.metadata, ip_address: log.ip_address, inserted_at: log.inserted_at } end) end end