towerops/lib/towerops_web/plugs/update_session_activity.ex
Graham McIntire 701ce12f08 perf+refactor: codebase-wide query and antipattern audit
Performance:
- schedule_live: preload page once instead of get_schedule!/1 per row
- alert_live + alerts: DB-side status filter; Repo.aggregate counts replace length/Enum.count over 500-row fetches on every event
- dashboard_live: drop duplicate get_device_status_counts; cap active alerts to 20 + use count_active_alerts/1
- maintenance.active_windows_for_device: 3 round-trips collapsed into one query (per-site-id branch keeps Postgres parameter types unambiguous)
- sites.build_site_tree: O(N^2) -> O(N) via group_by(parent_site_id)
- accounts.sole_owner_organizations: single group_by + having instead of per-org Repo.aggregate loop
- agents + agent_live (org + admin): count_assigned_devices_batch/1 + count_agent_polling_targets/1 (no preloads)
- alert_digest_worker: list_alerts_by_ids/1 batches digest fetch
- gaiia: distinct: true at DB; limit 50 on bidirectional ilike

Indexes:
- maintenance_windows(organization_id, starts_at, ends_at) WHERE suppress_alerts = true
- alerts(check_id) WHERE resolved_at IS NULL

Antipatterns:
- agents.delete_agent_token: PubSub.broadcast moved outside Repo.transaction so a rollback no longer leaves subscribers acting on a non-existent deletion
- integrations_controller.to_atom_keys: replaced String.to_existing_atom on user-controlled JSON keys with explicit allowlist
- 9 Task.start callsites converted to Task.Supervisor.start_child(Towerops.TaskSupervisor, ...) so background DB writes survive shutdown (test-mode discovery shims left as-is for sandbox semantics)
2026-04-28 16:58:51 -05:00

85 lines
2.7 KiB
Elixir

defmodule ToweropsWeb.Plugs.UpdateSessionActivity do
@moduledoc """
Updates the last_activity_at timestamp for the current browser session on each request.
This plug tracks session activity and enforces an inactivity timeout of 30 minutes.
If a session has been inactive for longer than the timeout, the user is automatically
logged out for security purposes.
The activity timestamp is used in the Sessions tab of User Settings to show when
each session was last active. The update happens in a background task to avoid
slowing down requests.
"""
import Plug.Conn
alias Towerops.Accounts
# 30 minutes of inactivity before automatic logout
@inactivity_timeout_seconds 30 * 60
@doc """
Initializes the plug with options (currently unused).
"""
def init(opts), do: opts
@doc """
Updates the last_activity_at timestamp for the current browser session.
Checks for session inactivity timeout and revokes the token if the session
has been inactive for more than 30 minutes. All database work runs in a
background task so the request is not blocked.
When a timed-out session is detected, the token is deleted so the next
request will be unauthenticated and redirected to login by the auth pipeline.
Looks up the session by the token stored in the session cookie, then
updates the timestamp in a background task.
"""
def call(conn, _opts) do
case get_session(conn, :user_token) do
nil ->
conn
token ->
live_socket_id = get_session(conn, :live_socket_id)
_ =
Task.Supervisor.start_child(Towerops.TaskSupervisor, fn ->
handle_session_activity(token, live_socket_id)
end)
conn
end
end
# Runs in a background task: check activity and either touch or revoke the session
defp handle_session_activity(token, live_socket_id) do
case Accounts.get_browser_session_by_token_value(token) do
nil ->
:ok
session ->
if session_timed_out?(session) do
revoke_inactive_session(token, live_socket_id)
else
Accounts.touch_browser_session(session)
end
end
end
# Check if session has exceeded inactivity timeout
defp session_timed_out?(session) do
last_activity = session.last_activity_at || session.inserted_at
inactive_seconds = DateTime.diff(DateTime.utc_now(), last_activity, :second)
inactive_seconds > @inactivity_timeout_seconds
end
# Revoke the token so the next request will be unauthenticated
defp revoke_inactive_session(token, live_socket_id) do
Accounts.delete_user_session_token(token)
if live_socket_id do
ToweropsWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
end
end