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)
85 lines
2.7 KiB
Elixir
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
|