defmodule ToweropsWeb.AccountLive.MyData do @moduledoc """ LiveView for displaying all user data (GDPR Right to Access) """ use ToweropsWeb, :live_view alias Towerops.Accounts alias Towerops.Accounts.Scope alias Towerops.Admin alias Towerops.Alerts alias Towerops.Devices alias Towerops.Organizations @impl true def handle_event("revoke_consent", %{"consent-id" => consent_id}, socket) do case Accounts.revoke_consent(consent_id) do {:ok, _consent} -> user = socket.assigns.current_scope.user socket = socket |> put_flash(:info, t("Consent revoked successfully.")) |> assign(:user_data, Map.put(socket.assigns.user_data, :consents, get_user_consents(user))) {:noreply, socket} {:error, _} -> {:noreply, put_flash(socket, :error, t("Failed to revoke consent."))} end end @impl true def mount(_params, _session, socket) do user = socket.assigns.current_scope.user # Get organizations and check ownership organizations = get_user_organizations(user) is_owner = Enum.any?(organizations, fn org -> org[:is_owner] end) default_organization = organizations |> Enum.map(& &1[:organization]) |> List.first() # Gather user data - only include org/devices/alerts if user is an owner user_data = %{ profile: get_user_profile(user), consents: get_user_consents(user), organizations: if(is_owner, do: organizations, else: []), devices: if(is_owner, do: get_user_devices(user), else: []), alerts: if(is_owner, do: get_user_alerts(user), else: []), audit_logs: get_user_audit_logs(user) } # Update scope with organization scope = Scope.put_organization(socket.assigns.current_scope, default_organization) {:ok, socket |> assign(:current_scope, scope) |> assign(:user_data, user_data) |> assign(:page_title, t("My Data")) |> assign(:is_owner, is_owner)} end @impl true def render(assigns) do ~H"""

Account Settings

Manage your account email address, password, and security settings

This page shows all personal data we have stored about you in compliance with GDPR Article 15 (Right to Access).

Profile Information

Email Address
{@user_data.profile.email}
First Name
{@user_data.profile.first_name || "Not set"}
Last Name
{@user_data.profile.last_name || "Not set"}
Timezone
{@user_data.profile.timezone}
Account Created
{ToweropsWeb.TimeHelpers.format_datetime( @user_data.profile.inserted_at, @current_scope.timezone, @current_scope.time_format )}
Email Confirmed
{if @user_data.profile.confirmed_at, do: "Yes", else: "No"} <%= if @user_data.profile.confirmed_at do %> ({ToweropsWeb.TimeHelpers.format_date( @user_data.profile.confirmed_at, @current_scope.timezone )}) <% end %>

Consent Records

Your consent to our Privacy Policy and Terms of Service

<%= if Enum.empty?(@user_data.consents) do %>

No consent records found.

<% else %>
<%= for consent <- @user_data.consents do %>
Type
{String.replace(consent.consent_type, "_", " ") |> String.capitalize()}
Version
{consent.version}
Granted
{ToweropsWeb.TimeHelpers.format_datetime( consent.granted_at, @current_scope.timezone, @current_scope.time_format )}
Status
<%= if consent.revoked_at do %> Revoked {ToweropsWeb.TimeHelpers.format_date( consent.revoked_at, @current_scope.timezone )} <% else %> Active <% end %>
<%= if !consent.revoked_at do %>
<% end %>
<% end %>
<% end %>
<%= if @is_owner do %>

Organizations

Organizations you own or are a member of

<%= if Enum.empty?(@user_data.organizations) do %>

No organization memberships.

<% else %>
<%= for org <- @user_data.organizations do %>
Name
{org.name}
Role
{if org.is_owner, do: "Owner", else: "Member"}
Slug
{org.slug}
Created
{ToweropsWeb.TimeHelpers.format_date( org.inserted_at, @current_scope.timezone )}
<% end %>
<% end %>

Devices

Network devices monitored by your organizations

<%= if Enum.empty?(@user_data.devices) do %>

No devices configured.

<% else %>
<%= for device <- @user_data.devices do %> <% end %>
Name IP Address Organization Created
{device.name} {device.ip_address} {device.site.organization.name} {ToweropsWeb.TimeHelpers.format_date( device.inserted_at, @current_scope.timezone )}

Total devices: {length(@user_data.devices)}

<% end %>

Recent Alerts

Alerts generated for your devices (last 90 days)

<%= if Enum.empty?(@user_data.alerts) do %>

No alerts in the last 90 days.

<% else %>
<%= for alert <- Enum.take(@user_data.alerts, 50) do %> <% end %>
Device Type Status Created
{alert.device.name} {alert.alert_type} {alert_status_text(alert)} {ToweropsWeb.TimeHelpers.format_datetime( alert.inserted_at, @current_scope.timezone, @current_scope.time_format )}

Showing {min(50, length(@user_data.alerts))} of {length(@user_data.alerts)} alerts

<% end %>
<% end %>

Audit Log

Administrative actions related to your account

<%= if Enum.empty?(@user_data.audit_logs) do %>

No audit log entries.

<% else %>
<%= for log <- Enum.take(@user_data.audit_logs, 20) do %>

{log.action}

By: {if log.superuser, do: log.superuser.email, else: "System"}

{ToweropsWeb.TimeHelpers.format_datetime( log.inserted_at, @current_scope.timezone, @current_scope.time_format )}

<% end %>

Showing {min(20, length(@user_data.audit_logs))} of {length(@user_data.audit_logs)} entries

<% end %>
<.icon name="hero-arrow-down-tray" class="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0" />

Export Your Data

You can download all your data in JSON format. This includes your profile, organizations, devices, alerts, and audit logs.

<.link href={~p"/api/v1/account/data"} class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" > <.icon name="hero-arrow-down-tray" class="h-4 w-4" /> Download My Data (JSON)

How We Use Your Data

  • Profile Information: Used to identify your account and personalize your experience
  • Email Address: Used for account notifications, password resets, and important service updates
  • Organizations: Determines which devices and alerts you can access
  • Device Data: Stored to provide network monitoring and alerting services
  • Monitoring Data: Time-series metrics used to track device health and performance
  • Audit Logs: Security records of administrative actions for compliance and security purposes

For more information, please read our <.link navigate={~p"/privacy"} class="text-blue-600 hover:text-blue-500 dark:text-blue-400" > Privacy Policy .

""" end # Helper function to get user profile data defp get_user_profile(user) do %{ email: user.email, first_name: user.first_name, last_name: user.last_name, timezone: user.timezone, inserted_at: user.inserted_at, confirmed_at: user.confirmed_at } end # Helper function to get user's organizations defp get_user_organizations(user) do user.id |> Organizations.list_user_organizations() |> Enum.map(fn org -> # Organization has the user's membership preloaded (only their membership) [membership] = org.memberships %{ name: org.name, slug: org.slug, is_owner: membership.role == :owner, inserted_at: org.inserted_at } end) end # Helper function to get devices from user's organizations defp get_user_devices(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() |> Towerops.Repo.preload(site: :organization) end end # Helper function to get alerts for user's devices (last 90 days) defp get_user_alerts(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) |> Towerops.Repo.preload(:device) end end # Helper function to get audit logs related to the user defp get_user_audit_logs(user) do Admin.list_audit_logs_for_user(user.id) end # Helper function to get user's consent records defp get_user_consents(user) do Accounts.list_user_consents(user.id) end # Helper function for alert status badge styling defp alert_status_class(alert) do cond do alert.resolved_at -> "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" alert.acknowledged_at -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400" true -> "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400" end end defp alert_status_text(alert) do cond do alert.resolved_at -> "Resolved" alert.acknowledged_at -> "Acknowledged" true -> "Active" end end end