cover/Elixir.WalletAuth.Session.SessionStore.html

1 defmodule WalletAuth.Session.SessionStore do
2 @moduledoc """
3 ETS-backed GenServer for session lifecycle management.
4
5 Manages active and revoked auth sessions.
6 On revocation, the session record is updated (not deleted) for audit traceability.
7
8 Per ADR 0006 and checklist Track D:
9 - Session start emits AuthSessionStarted event.
10 - Session revocation emits AuthSessionRevoked event.
11 """
12
13 use GenServer
14
15 alias WalletAuth.Session.Session
16
17 @table :wallet_auth_session_store
18
19 # --- Client API ---
20
21 def start_link(opts) do
22
:-(
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
23 end
24
25 @doc "Stores a new session. Returns :ok."
26 @spec store(Session.t()) :: :ok
27 def store(%Session{} = session) do
28 10 GenServer.call(__MODULE__, {:store, session})
29 end
30
31 @doc """
32 Looks up a session by session_id.
33 Returns `{:ok, session}` or `{:error, :not_found | :revoked}`.
34 """
35 @spec lookup(String.t()) :: {:ok, Session.t()} | {:error, :not_found | :revoked}
36 def lookup(session_id) do
37 7 GenServer.call(__MODULE__, {:lookup, session_id})
38 end
39
40 @doc "Revokes a session by session_id. Returns `{:ok, revoked_session}` or `{:error, :not_found}`."
41 @spec revoke(String.t()) :: {:ok, Session.t()} | {:error, :not_found}
42 def revoke(session_id) do
43 4 GenServer.call(__MODULE__, {:revoke, session_id})
44 end
45
46 @doc "Lists all active sessions for a subject (user_id)."
47 @spec list_active(sub :: String.t()) :: [Session.t()]
48 def list_active(sub) do
49 1 GenServer.call(__MODULE__, {:list_active, sub})
50 end
51
52 @doc "Resets all session store state. For test use only."
53 18 def reset, do: GenServer.call(__MODULE__, :reset)
54
55 # --- Server Callbacks ---
56
57 @impl true
58 def init(_opts) do
59
:-(
table = :ets.new(@table, [:set, :protected, :named_table])
60 {:ok, %{table: table}}
61 end
62
63 @impl true
64 def handle_call({:store, session}, _from, state) do
65 10 :ets.insert(@table, {session.session_id, session})
66 10 {:reply, :ok, state}
67 end
68
69 @impl true
70 def handle_call({:lookup, session_id}, _from, state) do
71 7 result =
72 case :ets.lookup(@table, session_id) do
73 1 [] -> {:error, :not_found}
74 2 [{_key, %Session{status: :revoked}}] -> {:error, :revoked}
75 4 [{_key, session}] -> {:ok, session}
76 end
77
78 7 {:reply, result, state}
79 end
80
81 @impl true
82 def handle_call({:revoke, session_id}, _from, state) do
83 4 result =
84 case :ets.lookup(@table, session_id) do
85 1 [] ->
86 {:error, :not_found}
87
88 [{_key, session}] ->
89 3 revoked = Session.revoke(session)
90 3 :ets.insert(@table, {session_id, revoked})
91 {:ok, revoked}
92 end
93
94 4 {:reply, result, state}
95 end
96
97 @impl true
98 def handle_call({:list_active, sub}, _from, state) do
99 1 sessions =
100 :ets.foldl(
101 fn {_key, session}, acc ->
102 3 if session.sub == sub and Session.active?(session), do: [session | acc], else: acc
103 end,
104 [],
105 @table
106 )
107
108 1 {:reply, sessions, state}
109 end
110
111 @impl true
112 def handle_call(:reset, _from, state) do
113 18 :ets.delete_all_objects(@table)
114 18 {:reply, :ok, state}
115 end
116 end
Line Hits Source