cover/Elixir.WalletAuth.Device.DeviceStore.html

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