This is the approach I came up with. It’s a GenServer that starts a timer for each user when they log in and then resets that timer with JavaScript on mousemove, etc. I’m not a fan of having to use a GenServer for this, and I don’t like solely relying on JavaScript for the reason @spacebat mentioned. Has anyone come up with a cleaner solution?
defmodule MyApp.ActivityTimer do
use GenServer
@moduledoc """
A GenServer that logs the user out after a specified inactivity timeout.
"""
def start_link(_opts) do
minutes =
Application.get_env(:MyApp, MyAppWeb.Session)[:inactivity_timeout_in_minutes] || 5
timeout = trunc(minutes * 60_000)
enabled = Application.get_env(:MyApp, MyAppWeb.Session)[:activity_timer_enabled] == true
GenServer.start_link(__MODULE__, %{timeout: timeout, timers: %{}, enabled: enabled},
name: __MODULE__
)
end
def init(state) do
{:ok, state}
end
def start_timer(user_token_id) do
GenServer.call(__MODULE__, {:start_timer, user_token_id})
end
def cancel_timer(user_token_id) do
GenServer.call(__MODULE__, {:cancel_timer, user_token_id})
end
@doc """
Changes the timeout value in the GenServer state.
The timeout value is in milliseconds.
"""
def set_timeout(timeout_ms) when is_integer(timeout_ms) and timeout_ms > 0 do
GenServer.call(__MODULE__, {:set_timeout, timeout_ms})
end
@doc """
Enable or disable the activity timer regardless of the config setting.
This is useful for testing.
"""
def set_enabled(enabled) when is_boolean(enabled) do
GenServer.call(__MODULE__, {:set_enabled, enabled})
end
def handle_call({:set_timeout, timeout_ms}, _from, state) do
{:reply, :ok, %{state | timeout: timeout_ms}}
end
def handle_call({:set_enabled, enabled}, _from, state) do
{:reply, :ok, %{state | enabled: enabled}}
end
def handle_call(
{:start_timer, user_token_id},
_from,
%{timeout: timeout, timers: timers, enabled: true} = state
) do
if Map.has_key?(timers, user_token_id) do
Process.cancel_timer(timers[user_token_id])
end
timer_ref = Process.send_after(self(), {:timeout, user_token_id}, timeout)
{:reply, :ok, %{state | timers: Map.put(timers, user_token_id, timer_ref)}}
end
def handle_call(
{:cancel_timer, user_token_id},
_from,
%{timers: timers, enabled: true} = state
) do
if Map.has_key?(timers, user_token_id) do
Process.cancel_timer(timers[user_token_id])
{:reply, :ok, %{state | timers: Map.delete(timers, user_token_id)}}
else
{:reply, :ok, state}
end
end
def handle_call(_request, _from, %{enabled: false} = state) do
require Logger
Logger.debug("Activity timer is disabled, ignoring request.")
{:reply, :ok, state}
end
def handle_info({:timeout, user_token_id}, %{timers: timers} = state) do
Task.start(fn -> expire_user_token_id(user_token_id) end)
{:noreply, %{state | timers: Map.delete(timers, user_token_id)}}
end
defp expire_user_token_id(user_token_id) do
MyApp.Users.delete_user_session_token(user_token_id)
MyAppWeb.Endpoint.broadcast(
"users_sessions:#{Base.url_encode64(user_token_id)}",
"disconnect",
%{}
)
end
end