1use oxidite_db::sqlx::{self, FromRow};
2use sha2::{Sha256, Digest};
3use rand::Rng;
4use base64::Engine;
5
6#[derive(FromRow, Clone, Debug)]
7pub struct ApiKey {
8 pub id: i64,
9 pub user_id: i64,
10 pub key_hash: String,
11 pub name: String,
12 pub last_used_at: Option<i64>,
13 pub expires_at: Option<i64>,
14 pub created_at: i64,
15 pub updated_at: i64,
16}
17
18impl ApiKey {
19 pub fn generate_key() -> String {
21 let mut rng = rand::rng();
22 let random_bytes: Vec<u8> = (0..32).map(|_| rng.random()).collect();
23 let key = base64::engine::general_purpose::URL_SAFE_NO_PAD
24 .encode(&random_bytes);
25 format!("ox_{}", key)
26 }
27
28 pub fn hash_key(key: &str) -> String {
30 let mut hasher = Sha256::new();
31 hasher.update(key.as_bytes());
32 format!("{:x}", hasher.finalize())
33 }
34
35 pub async fn create_for_user<D: oxidite_db::Database>(
37 db: &D,
38 user_id: i64,
39 name: &str,
40 expires_at: Option<i64>,
41 ) -> oxidite_db::Result<(ApiKey, String)> {
42 let key = Self::generate_key();
43 let key_hash = Self::hash_key(&key);
44 let now = chrono::Utc::now().timestamp();
45
46 let query = oxidite_db::sqlx::query(
47 "INSERT INTO api_keys (user_id, key_hash, name, expires_at, created_at, updated_at)
48 VALUES (?, ?, ?, ?, ?, ?)"
49 )
50 .bind(user_id)
51 .bind(&key_hash)
52 .bind(name)
53 .bind(expires_at)
54 .bind(now)
55 .bind(now);
56
57 db.execute_query(query).await?;
58
59 let get_query = oxidite_db::sqlx::query(
61 "SELECT * FROM api_keys WHERE key_hash = ?"
62 )
63 .bind(&key_hash);
64 let row = db.fetch_one(get_query).await?
65 .ok_or_else(|| sqlx::Error::RowNotFound)?;
66
67 let api_key = ApiKey::from_row(&row)?;
68 Ok((api_key, key))
69 }
70
71 pub async fn verify_key<D: oxidite_db::Database + ?Sized>(
73 db: &D,
74 key: &str,
75 ) -> oxidite_db::Result<Option<ApiKey>> {
76 let key_hash = Self::hash_key(key);
77 let now = chrono::Utc::now().timestamp();
78
79 let query = oxidite_db::sqlx::query(
80 "SELECT * FROM api_keys
81 WHERE key_hash = ?
82 AND (expires_at IS NULL OR expires_at > ?)"
83 )
84 .bind(&key_hash)
85 .bind(now);
86
87 let row = db.fetch_one(query).await?;
88
89 match row {
90 Some(row) => {
91 let mut api_key = ApiKey::from_row(&row)?;
92
93 let update_query = oxidite_db::sqlx::query(
95 "UPDATE api_keys SET last_used_at = ? WHERE id = ?"
96 )
97 .bind(now)
98 .bind(api_key.id);
99 let _ = db.execute_query(update_query).await;
100 api_key.last_used_at = Some(now);
101
102 Ok(Some(api_key))
103 }
104 None => Ok(None),
105 }
106 }
107
108 pub async fn revoke<D: oxidite_db::Database>(
110 db: &D,
111 key_id: i64,
112 user_id: i64,
113 ) -> oxidite_db::Result<bool> {
114 let query = oxidite_db::sqlx::query(
115 "DELETE FROM api_keys WHERE id = ? AND user_id = ?"
116 )
117 .bind(key_id)
118 .bind(user_id);
119 let rows = db.execute_query(query).await?;
120 Ok(rows > 0)
121 }
122
123 pub async fn get_user_keys<D: oxidite_db::Database>(
125 db: &D,
126 user_id: i64,
127 ) -> oxidite_db::Result<Vec<ApiKey>> {
128 let query = oxidite_db::sqlx::query(
129 "SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at DESC"
130 )
131 .bind(user_id);
132
133 let rows = db.fetch_all(query).await?;
134 let mut keys = Vec::new();
135
136 for row in rows {
137 keys.push(ApiKey::from_row(&row)?);
138 }
139
140 Ok(keys)
141 }
142}