1use rusqlite::{Connection, params};
8use subtle::ConstantTimeEq;
9
10use crate::auth::{self, Role};
11
12use super::auth::UserRow;
13
14const TOUCH_INTERVAL_SECS: i64 = 60;
17
18#[derive(Debug, Clone)]
19pub struct ApiKeyRow {
20 pub id: i64,
21 pub user_id: i64,
22 pub username: String,
23 pub name: String,
24 pub created_at: i64,
25 pub last_used_at: Option<i64>,
26}
27
28pub fn create_api_key(
31 conn: &Connection,
32 user_id: i64,
33 name: &str,
34) -> Result<(i64, String), rusqlite::Error> {
35 let key =
36 auth::random_api_key().map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?;
37 conn.execute(
38 "INSERT INTO api_keys (user_id, name, key_hash, created_at) VALUES (?1, ?2, ?3, ?4)",
39 params![
40 user_id,
41 name,
42 auth::sha256_hex(&key),
43 auth::now_unix() as i64
44 ],
45 )?;
46 Ok((conn.last_insert_rowid(), key))
47}
48
49pub fn list_api_keys(
51 conn: &Connection,
52 user_id: Option<i64>,
53) -> Result<Vec<ApiKeyRow>, rusqlite::Error> {
54 let mut stmt = conn.prepare(
55 "SELECT k.id, k.user_id, u.username, k.name, k.created_at, k.last_used_at
56 FROM api_keys k JOIN users u ON u.id = k.user_id
57 WHERE ?1 IS NULL OR k.user_id = ?1
58 ORDER BY k.id",
59 )?;
60 let rows = stmt.query_map(params![user_id], |row| {
61 Ok(ApiKeyRow {
62 id: row.get(0)?,
63 user_id: row.get(1)?,
64 username: row.get(2)?,
65 name: row.get(3)?,
66 created_at: row.get(4)?,
67 last_used_at: row.get(5)?,
68 })
69 })?;
70 rows.collect()
71}
72
73pub fn revoke_api_key(
76 conn: &Connection,
77 id: i64,
78 user_id: Option<i64>,
79) -> Result<bool, rusqlite::Error> {
80 let n = conn.execute(
81 "DELETE FROM api_keys WHERE id = ?1 AND (?2 IS NULL OR user_id = ?2)",
82 params![id, user_id],
83 )?;
84 Ok(n > 0)
85}
86
87pub fn revoke_api_key_value(conn: &Connection, key: &str) -> Result<bool, rusqlite::Error> {
90 let n = conn.execute(
91 "DELETE FROM api_keys WHERE key_hash = ?1",
92 params![auth::sha256_hex(key)],
93 )?;
94 Ok(n > 0)
95}
96
97pub fn revoke_user_api_keys(conn: &Connection, user_id: i64) -> Result<usize, rusqlite::Error> {
99 conn.execute("DELETE FROM api_keys WHERE user_id = ?1", params![user_id])
100}
101
102pub fn authenticate_api_key(
108 conn: &Connection,
109 key: &str,
110) -> Result<Option<UserRow>, rusqlite::Error> {
111 let given = auth::sha256_hex(key);
112 let mut found = None;
113 {
114 let mut stmt = conn.prepare_cached("SELECT id, key_hash, last_used_at FROM api_keys")?;
115 let rows = stmt.query_map([], |row| {
116 Ok((
117 row.get::<_, i64>(0)?,
118 row.get::<_, String>(1)?,
119 row.get::<_, Option<i64>>(2)?,
120 ))
121 })?;
122 for row in rows {
123 let (id, hash, last_used) = row?;
124 if bool::from(hash.as_bytes().ct_eq(given.as_bytes())) {
125 found = Some((id, last_used));
126 }
127 }
128 }
129 let Some((id, last_used)) = found else {
130 return Ok(None);
131 };
132
133 let now = auth::now_unix() as i64;
137 if last_used.is_none_or(|t| t <= now - TOUCH_INTERVAL_SECS)
138 && let Err(e) = crate::db::connection::without_waiting(conn, |conn| {
139 conn.execute(
140 "UPDATE api_keys SET last_used_at = ?1 WHERE id = ?2",
141 params![now, id],
142 )
143 })
144 {
145 log::debug!("api key {id}: last use not recorded: {e}");
146 }
147
148 let mut stmt = conn.prepare_cached(
149 "SELECT u.id, u.username, u.password_hash, u.role, u.created_at
150 FROM api_keys k JOIN users u ON u.id = k.user_id WHERE k.id = ?1",
151 )?;
152 let mut rows = stmt.query_map(params![id], |row| {
153 let role: String = row.get(3)?;
154 Ok(UserRow {
155 id: row.get(0)?,
156 username: row.get(1)?,
157 password_hash: row.get(2)?,
158 role: role.parse().unwrap_or(Role::Readonly),
159 created_at: row.get(4)?,
160 })
161 })?;
162 rows.next().transpose()
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::db::connection::Database;
169 use crate::db::queries::auth::{create_user, delete_user, update_role};
170
171 fn test_db() -> (Database, tempfile::TempDir) {
172 let tmp = tempfile::TempDir::new().unwrap();
173 let db = Database::open(&tmp.path().join("test.db")).unwrap();
174 (db, tmp)
175 }
176
177 #[test]
178 fn key_signs_in_as_its_user_at_their_current_role() {
179 let (db, _tmp) = test_db();
180 let uid = create_user(&db.conn, "alice", "pw", Role::User).unwrap();
181 let (_, key) = create_api_key(&db.conn, uid, "phone").unwrap();
182 assert_eq!(key.len(), 43);
183
184 let user = authenticate_api_key(&db.conn, &key).unwrap().unwrap();
185 assert_eq!((user.username.as_str(), user.role), ("alice", Role::User));
186
187 update_role(&db.conn, "alice", Role::Readonly).unwrap();
188 let user = authenticate_api_key(&db.conn, &key).unwrap().unwrap();
189 assert_eq!(user.role, Role::Readonly);
190
191 assert!(authenticate_api_key(&db.conn, "nope").unwrap().is_none());
192 }
193
194 #[test]
195 fn only_the_hash_is_stored_and_use_is_recorded() {
196 let (db, _tmp) = test_db();
197 let uid = create_user(&db.conn, "alice", "pw", Role::User).unwrap();
198 let (_, key) = create_api_key(&db.conn, uid, "phone").unwrap();
199 let stored: String = db
200 .conn
201 .query_row("SELECT key_hash FROM api_keys", [], |r| r.get(0))
202 .unwrap();
203 assert_eq!(stored, auth::sha256_hex(&key));
204
205 assert!(
206 list_api_keys(&db.conn, None).unwrap()[0]
207 .last_used_at
208 .is_none()
209 );
210 authenticate_api_key(&db.conn, &key).unwrap();
211 assert!(
212 list_api_keys(&db.conn, None).unwrap()[0]
213 .last_used_at
214 .is_some()
215 );
216 }
217
218 #[test]
221 fn a_held_write_lock_does_not_hold_up_sign_in() {
222 let (db, tmp) = test_db();
223 let uid = create_user(&db.conn, "alice", "pw", Role::User).unwrap();
224 let (_, key) = create_api_key(&db.conn, uid, "phone").unwrap();
225
226 let writer = Database::open_existing(&tmp.path().join("test.db")).unwrap();
227 writer.conn.execute_batch("BEGIN IMMEDIATE").unwrap();
228 let started = std::time::Instant::now();
229 assert!(authenticate_api_key(&db.conn, &key).unwrap().is_some());
230 assert!(started.elapsed() < std::time::Duration::from_secs(5));
231 writer.conn.execute_batch("ROLLBACK").unwrap();
232
233 authenticate_api_key(&db.conn, &key).unwrap();
234 writer.conn.execute_batch("BEGIN IMMEDIATE").unwrap();
235 let started = std::time::Instant::now();
236 assert!(authenticate_api_key(&db.conn, &key).unwrap().is_some());
237 assert!(started.elapsed() < std::time::Duration::from_secs(5));
238 writer.conn.execute_batch("ROLLBACK").unwrap();
239 }
240
241 #[test]
242 fn revoke_is_scoped_and_users_take_their_keys_with_them() {
243 let (db, _tmp) = test_db();
244 let alice = create_user(&db.conn, "alice", "pw", Role::User).unwrap();
245 let bob = create_user(&db.conn, "bob", "pw", Role::User).unwrap();
246 let (a, _) = create_api_key(&db.conn, alice, "a").unwrap();
247 let (_, bob_key) = create_api_key(&db.conn, bob, "b").unwrap();
248
249 assert!(!revoke_api_key(&db.conn, a, Some(bob)).unwrap());
250 assert!(revoke_api_key(&db.conn, a, Some(alice)).unwrap());
251 assert_eq!(list_api_keys(&db.conn, Some(alice)).unwrap().len(), 0);
252
253 delete_user(&db.conn, bob).unwrap();
254 assert!(authenticate_api_key(&db.conn, &bob_key).unwrap().is_none());
255 assert!(list_api_keys(&db.conn, None).unwrap().is_empty());
256 }
257}