agentmux_srv/backend/storage/
viewer_devices.rs1use rusqlite::params;
13
14use super::error::StoreError;
15use super::store::Store;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ViewerDevice {
20 pub device_id: String,
21 pub device_name: String,
22 pub device_key: String,
24 pub created_ms: i64,
25 pub last_seen_ms: i64,
27}
28
29impl Store {
30 pub fn viewer_device_insert(
32 &self,
33 device_id: &str,
34 token_hash: &str,
35 device_name: &str,
36 device_key: &str,
37 created_ms: i64,
38 ) -> Result<(), StoreError> {
39 let conn = self.conn.lock().unwrap();
40 conn.execute(
41 "INSERT INTO db_viewer_devices \
42 (device_id, token_hash, device_name, device_key, created_ms, last_seen_ms) \
43 VALUES (?1, ?2, ?3, ?4, ?5, 0)",
44 params![device_id, token_hash, device_name, device_key, created_ms],
45 )?;
46 Ok(())
47 }
48
49 pub fn viewer_device_list(&self) -> Result<Vec<ViewerDevice>, StoreError> {
51 let conn = self.conn.lock().unwrap();
52 let mut stmt = conn.prepare(
53 "SELECT device_id, device_name, device_key, created_ms, last_seen_ms \
54 FROM db_viewer_devices ORDER BY created_ms, device_id",
55 )?;
56 let rows = stmt.query_map([], |row| {
57 Ok(ViewerDevice {
58 device_id: row.get(0)?,
59 device_name: row.get(1)?,
60 device_key: row.get(2)?,
61 created_ms: row.get(3)?,
62 last_seen_ms: row.get(4)?,
63 })
64 })?;
65 Ok(rows.collect::<Result<Vec<_>, _>>()?)
66 }
67
68 pub fn viewer_device_token_hashes(&self) -> Result<Vec<(String, String)>, StoreError> {
71 let conn = self.conn.lock().unwrap();
72 let mut stmt = conn.prepare("SELECT device_id, token_hash FROM db_viewer_devices")?;
73 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
74 Ok(rows.collect::<Result<Vec<_>, _>>()?)
75 }
76
77 pub fn viewer_device_touch(&self, device_id: &str, last_seen_ms: i64) -> Result<bool, StoreError> {
79 let conn = self.conn.lock().unwrap();
80 let rows = conn.execute(
81 "UPDATE db_viewer_devices SET last_seen_ms = ?2 WHERE device_id = ?1",
82 params![device_id, last_seen_ms],
83 )?;
84 Ok(rows > 0)
85 }
86
87 pub fn viewer_device_delete(&self, device_id: &str) -> Result<bool, StoreError> {
89 let conn = self.conn.lock().unwrap();
90 let rows = conn.execute("DELETE FROM db_viewer_devices WHERE device_id = ?1", params![device_id])?;
91 Ok(rows > 0)
92 }
93
94 pub fn agent_hide_from_devices_get(&self, id: &str) -> Result<bool, StoreError> {
97 let conn = self.conn.lock().unwrap();
98 match conn.query_row(
99 "SELECT hide_from_devices FROM db_agents WHERE id = ?1",
100 params![id],
101 |row| row.get::<_, i64>(0),
102 ) {
103 Ok(v) => Ok(v != 0),
104 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
105 Err(e) => Err(e.into()),
106 }
107 }
108
109 pub fn agent_hide_from_devices_set(&self, id: &str, hidden: bool) -> Result<bool, StoreError> {
113 let conn = self.conn.lock().unwrap();
114 let rows = conn.execute(
115 "UPDATE db_agents SET hide_from_devices = ?2 WHERE id = ?1",
116 params![id, hidden as i64],
117 )?;
118 Ok(rows > 0)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn devices_are_listed_touched_and_revoked() {
128 let store = Store::open_in_memory().unwrap();
129 assert!(store.viewer_device_list().unwrap().is_empty());
130 store.viewer_device_insert("d1", "aa", "Pixel", "", 100).unwrap();
131 store.viewer_device_insert("d2", "bb", "Tablet", "a2V5", 200).unwrap();
132 let list = store.viewer_device_list().unwrap();
133 assert_eq!(list.len(), 2);
134 assert_eq!(list[0].device_name, "Pixel");
135 assert_eq!(list[1].device_key, "a2V5");
136 assert_eq!(list[0].last_seen_ms, 0);
137
138 assert!(store.viewer_device_touch("d1", 5_000).unwrap());
139 assert_eq!(store.viewer_device_list().unwrap()[0].last_seen_ms, 5_000);
140 assert_eq!(
141 store.viewer_device_token_hashes().unwrap().len(),
142 2,
143 "the bearer check sees every device"
144 );
145
146 assert!(store.viewer_device_delete("d1").unwrap());
147 assert!(!store.viewer_device_delete("d1").unwrap(), "already gone");
148 assert!(!store.viewer_device_touch("d1", 6_000).unwrap());
149 assert_eq!(store.viewer_device_list().unwrap().len(), 1);
150 }
151
152 #[test]
153 fn a_token_hash_belongs_to_one_device() {
154 let store = Store::open_in_memory().unwrap();
155 store.viewer_device_insert("d1", "same", "A", "", 1).unwrap();
156 assert!(store.viewer_device_insert("d2", "same", "B", "", 2).is_err());
157 }
158
159 #[test]
160 fn the_hide_flag_defaults_off_and_is_kept_per_agent() {
161 let store = Store::open_in_memory().unwrap();
162 {
163 let conn = store.conn.lock().unwrap();
164 conn.execute_batch(
165 "INSERT INTO db_agents (id, name, provider, created_at) VALUES ('a1', 'One', 'claude', 0);
166 INSERT INTO db_agents (id, name, provider, created_at) VALUES ('a2', 'Two', 'claude', 0);",
167 )
168 .unwrap();
169 }
170 assert!(!store.agent_hide_from_devices_get("a1").unwrap());
171 assert!(store.agent_hide_from_devices_set("a1", true).unwrap());
172 assert!(store.agent_hide_from_devices_get("a1").unwrap());
173 assert!(!store.agent_hide_from_devices_get("a2").unwrap(), "another agent is unaffected");
174 assert!(store.agent_hide_from_devices_set("a1", false).unwrap());
175 assert!(!store.agent_hide_from_devices_get("a1").unwrap());
176 assert!(!store.agent_hide_from_devices_set("nope", true).unwrap());
177 assert!(!store.agent_hide_from_devices_get("nope").unwrap());
178 }
179}