diff --git a/lib/towerops/admin/audit_log.ex b/lib/towerops/admin/audit_log.ex
index bb600ccc..e92e7a44 100644
--- a/lib/towerops/admin/audit_log.ex
+++ b/lib/towerops/admin/audit_log.ex
@@ -15,6 +15,9 @@ defmodule Towerops.Admin.AuditLog do
field :action, :string
field :metadata, :map
field :ip_address, :string
+ field :user_agent, :string
+ field :request_path, :string
+ field :data_accessed, :map
belongs_to :superuser, User
belongs_to :target_user, User
@@ -25,15 +28,40 @@ defmodule Towerops.Admin.AuditLog do
@doc false
def changeset(audit_log, attrs) do
audit_log
- |> cast(attrs, [:action, :superuser_id, :target_user_id, :metadata, :ip_address])
- |> validate_required([:action, :superuser_id])
+ |> cast(attrs, [
+ :action,
+ :superuser_id,
+ :target_user_id,
+ :metadata,
+ :ip_address,
+ :user_agent,
+ :request_path,
+ :data_accessed
+ ])
+ |> validate_required([:action])
|> validate_inclusion(:action, [
+ # Superuser actions
"impersonate_start",
"impersonate_end",
"user_delete",
"org_delete",
"agent_debug_enable",
- "agent_debug_disable"
+ "agent_debug_disable",
+ # User data access
+ "user_data_viewed",
+ "user_data_exported",
+ "user_profile_updated",
+ # Organization actions
+ "org_data_accessed",
+ # Device actions
+ "device_created",
+ "device_updated",
+ "device_deleted",
+ # Monitoring actions
+ "monitoring_data_queried",
+ # Security events
+ "failed_access_attempt",
+ "privilege_escalation"
])
end
end
diff --git a/lib/towerops/admin/audit_logger.ex b/lib/towerops/admin/audit_logger.ex
new file mode 100644
index 00000000..adca20a6
--- /dev/null
+++ b/lib/towerops/admin/audit_logger.ex
@@ -0,0 +1,177 @@
+defmodule Towerops.Admin.AuditLogger do
+ @moduledoc """
+ Helper module for creating audit log entries with automatic extraction of
+ request metadata (IP address, user agent, request path).
+
+ This module provides convenience functions for logging various types of
+ audit events throughout the application.
+ """
+
+ alias Towerops.Admin
+
+ @doc """
+ Logs an audit event with automatic extraction of conn metadata.
+
+ ## Parameters
+
+ * `action` - String representing the action being logged
+ * `conn` - The Plug.Conn struct (can be nil for background jobs)
+ * `opts` - Keyword list of options:
+ * `:actor_id` - ID of the user performing the action (optional)
+ * `:target_user_id` - ID of the user being acted upon (optional)
+ * `:metadata` - Additional metadata map (optional)
+ * `:data_accessed` - Map of data fields that were accessed (optional)
+
+ ## Examples
+
+ iex> log_event("user_data_viewed", conn, actor_id: admin_id, target_user_id: user_id)
+ {:ok, %AuditLog{}}
+ """
+ def log_event(action, conn \\ nil, opts \\ []) do
+ attrs = build_attrs(action, conn, opts)
+ Admin.create_audit_log(attrs)
+ end
+
+ @doc """
+ Logs when a user's data is viewed by an admin or another user.
+ """
+ def log_user_data_viewed(conn, viewer_id, target_user_id, data_accessed) do
+ log_event("user_data_viewed", conn,
+ actor_id: viewer_id,
+ target_user_id: target_user_id,
+ data_accessed: data_accessed
+ )
+ end
+
+ @doc """
+ Logs when a user exports their data.
+ """
+ def log_user_data_exported(conn, user_id) do
+ log_event("user_data_exported", conn,
+ actor_id: user_id,
+ target_user_id: user_id,
+ metadata: %{export_format: "json"}
+ )
+ end
+
+ @doc """
+ Logs when a user's profile is updated.
+ """
+ def log_user_profile_updated(conn, user_id, fields_changed) do
+ log_event("user_profile_updated", conn,
+ actor_id: user_id,
+ target_user_id: user_id,
+ data_accessed: %{fields_changed: fields_changed}
+ )
+ end
+
+ @doc """
+ Logs when organization data is accessed.
+ """
+ def log_org_data_accessed(conn, user_id, org_id, action_detail) do
+ log_event("org_data_accessed", conn,
+ actor_id: user_id,
+ metadata: %{organization_id: org_id, action: action_detail}
+ )
+ end
+
+ @doc """
+ Logs device creation.
+ """
+ def log_device_created(conn, user_id, device_id, device_name) do
+ log_event("device_created", conn,
+ actor_id: user_id,
+ metadata: %{device_id: device_id, device_name: device_name}
+ )
+ end
+
+ @doc """
+ Logs device update.
+ """
+ def log_device_updated(conn, user_id, device_id, fields_changed) do
+ log_event("device_updated", conn,
+ actor_id: user_id,
+ metadata: %{device_id: device_id},
+ data_accessed: %{fields_changed: fields_changed}
+ )
+ end
+
+ @doc """
+ Logs device deletion.
+ """
+ def log_device_deleted(conn, user_id, device_id, device_name) do
+ log_event("device_deleted", conn,
+ actor_id: user_id,
+ metadata: %{device_id: device_id, device_name: device_name}
+ )
+ end
+
+ @doc """
+ Logs when monitoring data is queried.
+ """
+ def log_monitoring_data_queried(conn, user_id, device_id, time_range) do
+ log_event("monitoring_data_queried", conn,
+ actor_id: user_id,
+ metadata: %{device_id: device_id, time_range: time_range}
+ )
+ end
+
+ @doc """
+ Logs failed access attempts.
+ """
+ def log_failed_access_attempt(conn, user_id, resource, reason) do
+ log_event("failed_access_attempt", conn,
+ actor_id: user_id,
+ metadata: %{resource: resource, reason: reason}
+ )
+ end
+
+ @doc """
+ Logs privilege escalations (e.g., becoming org admin).
+ """
+ def log_privilege_escalation(conn, user_id, from_role, to_role, context) do
+ log_event("privilege_escalation", conn,
+ actor_id: user_id,
+ metadata: %{from_role: from_role, to_role: to_role, context: context}
+ )
+ end
+
+ # Private helper functions
+
+ defp build_attrs(action, conn, opts) do
+ base_attrs = %{
+ action: action,
+ superuser_id: Keyword.get(opts, :actor_id),
+ target_user_id: Keyword.get(opts, :target_user_id),
+ metadata: Keyword.get(opts, :metadata, %{}),
+ data_accessed: Keyword.get(opts, :data_accessed)
+ }
+
+ if conn do
+ base_attrs
+ |> Map.put(:ip_address, get_ip_address(conn))
+ |> Map.put(:user_agent, get_user_agent(conn))
+ |> Map.put(:request_path, get_request_path(conn))
+ else
+ base_attrs
+ end
+ end
+
+ defp get_ip_address(%Plug.Conn{} = conn) do
+ case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
+ [ip | _] -> ip |> String.split(",") |> List.first() |> String.trim()
+ [] -> to_string(:inet_parse.ntoa(conn.remote_ip))
+ end
+ end
+
+ defp get_user_agent(%Plug.Conn{} = conn) do
+ case Plug.Conn.get_req_header(conn, "user-agent") do
+ [ua | _] -> ua
+ [] -> nil
+ end
+ end
+
+ defp get_request_path(%Plug.Conn{} = conn) do
+ conn.request_path
+ end
+end
diff --git a/lib/towerops_web/controllers/api/account_data_controller.ex b/lib/towerops_web/controllers/api/account_data_controller.ex
index db684527..386683e6 100644
--- a/lib/towerops_web/controllers/api/account_data_controller.ex
+++ b/lib/towerops_web/controllers/api/account_data_controller.ex
@@ -6,6 +6,7 @@ defmodule ToweropsWeb.Api.AccountDataController do
alias Towerops.Accounts
alias Towerops.Admin
+ alias Towerops.Admin.AuditLogger
alias Towerops.Alerts
alias Towerops.Devices
alias Towerops.Organizations
@@ -16,6 +17,9 @@ defmodule ToweropsWeb.Api.AccountDataController do
def show(conn, _params) do
user = conn.assigns.current_scope.user
+ # 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),
diff --git a/lib/towerops_web/controllers/page_html/privacy.html.heex b/lib/towerops_web/controllers/page_html/privacy.html.heex
index 58525e02..b4e9a7c6 100644
--- a/lib/towerops_web/controllers/page_html/privacy.html.heex
+++ b/lib/towerops_web/controllers/page_html/privacy.html.heex
@@ -88,9 +88,112 @@
-
Cookies and Tracking
+
Cookie Policy
+
+
+ What Are Cookies?
+
- We use essential cookies to maintain your session and preferences. We do not use third-party tracking or advertising cookies.
+ Cookies are small text files that are placed on your device when you visit our website. They help us provide you with a better experience by remembering your preferences and keeping you logged in.
+
+
+
+ What Cookies Do We Use?
+
+
+ Towerops uses only essential cookies
+ that are strictly necessary for the operation of our platform. We do not use third-party tracking, advertising, or analytics cookies.
+
+
+
+
Essential Cookies
+
+ These cookies are required for our website to function properly and cannot be disabled:
+
+
+
+ Session Cookie (_towerops_web_key)
+
+
+ Purpose: Maintains your login session and authentication state
+
+
+
+ Duration: Expires when you close your browser or log out
+
+
+
+ CSRF Token
+
+
+ Purpose: Protects against Cross-Site Request Forgery attacks
+
+
+ Duration: Expires when you close your browser
+
+
+ LiveView Session
+
+
+ Purpose: Enables real-time updates and interactive features
+
+
+ Duration: Expires when you close your browser
+
+ We do not use any third-party cookies, tracking pixels, or analytics services that would place cookies on your device.
+
+
+
Managing Cookies
+
+ Since we only use essential cookies required for the website to function, disabling them will prevent you from using Towerops. However, you can manage cookies through your browser settings:
+
+
+
+ Chrome:
+ Settings → Privacy and security → Cookies and other site data
+
+
+ Firefox:
+ Settings → Privacy & Security → Cookies and Site Data
+
+ Edge:
+ Settings → Cookies and site permissions → Manage and delete cookies
+
+
+
+
+ Cookie Consent (EU/EEA Users)
+
+
+ If you are accessing Towerops from the European Union or European Economic Area, you will see a cookie consent banner on your first visit. Since we only use essential cookies necessary for the website to function, this banner is informational. Under GDPR, essential cookies do not require explicit consent as they are strictly necessary for the service you have requested.
+
+
+
+ Changes to Our Cookie Policy
+
+
+ If we introduce new types of cookies in the future (such as analytics or marketing cookies), we will update this policy and, where required, request your consent before using them.
diff --git a/lib/towerops_web/live/account_live/activity.ex b/lib/towerops_web/live/account_live/activity.ex
new file mode 100644
index 00000000..e9b76783
--- /dev/null
+++ b/lib/towerops_web/live/account_live/activity.ex
@@ -0,0 +1,278 @@
+defmodule ToweropsWeb.AccountLive.Activity do
+ @moduledoc """
+ LiveView for displaying user's audit log activity (GDPR transparency).
+ Shows both actions performed by the user and actions performed on the user's data.
+ """
+ use ToweropsWeb, :live_view
+
+ alias Towerops.Admin
+
+ @impl true
+ def mount(_params, _session, socket) do
+ user = socket.assigns.current_scope.user
+
+ # Get audit logs for this user (both as actor and target)
+ audit_logs = Admin.list_audit_logs_for_user(user.id, limit: 100)
+
+ {:ok,
+ socket
+ |> assign(:page_title, "Activity Log")
+ |> assign(:audit_logs, audit_logs)}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ Activity Log
+
+
+ View all actions performed by you and on your account data for transparency and security.
+
+ This activity log shows actions performed by you and on your account data.
+ Records are retained for 3 years for compliance and security purposes.
+