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 = format!(
47 "INSERT INTO api_keys (user_id, key_hash, name, expires_at, created_at, updated_at)
48 VALUES ({}, '{}', '{}', {}, {}, {})",
49 user_id, key_hash, name,
50 expires_at.map(|e| e.to_string()).unwrap_or("NULL".to_string()),
51 now, now
52 );
53
54 db.execute(&query).await?;
55
56 let get_query = format!(
58 "SELECT * FROM api_keys WHERE key_hash = '{}'",
59 key_hash
60 );
61 let row = db.query_one(&get_query).await?
62 .ok_or_else(|| sqlx::Error::RowNotFound)?;
63
64 let api_key = ApiKey::from_row(&row)?;
65 Ok((api_key, key))
66 }
67
68 pub async fn verify_key<D: oxidite_db::Database + ?Sized>(
70 db: &D,
71 key: &str,
72 ) -> oxidite_db::Result<Option<ApiKey>> {
73 let key_hash = Self::hash_key(key);
74 let now = chrono::Utc::now().timestamp();
75
76 let query = format!(
77 "SELECT * FROM api_keys
78 WHERE key_hash = '{}'
79 AND (expires_at IS NULL OR expires_at > {})",
80 key_hash, now
81 );
82
83 let row = db.query_one(&query).await?;
84
85 match row {
86 Some(row) => {
87 let mut api_key = ApiKey::from_row(&row)?;
88
89 let update_query = format!(
91 "UPDATE api_keys SET last_used_at = {} WHERE id = {}",
92 now, api_key.id
93 );
94 let _ = db.execute(&update_query).await;
95 api_key.last_used_at = Some(now);
96
97 Ok(Some(api_key))
98 }
99 None => Ok(None),
100 }
101 }
102
103 pub async fn revoke<D: oxidite_db::Database>(
105 db: &D,
106 key_id: i64,
107 user_id: i64,
108 ) -> oxidite_db::Result<bool> {
109 let query = format!(
110 "DELETE FROM api_keys WHERE id = {} AND user_id = {}",
111 key_id, user_id
112 );
113 let rows = db.execute(&query).await?;
114 Ok(rows > 0)
115 }
116
117 pub async fn get_user_keys<D: oxidite_db::Database>(
119 db: &D,
120 user_id: i64,
121 ) -> oxidite_db::Result<Vec<ApiKey>> {
122 let query = format!(
123 "SELECT * FROM api_keys WHERE user_id = {} ORDER BY created_at DESC",
124 user_id
125 );
126
127 let rows = db.query(&query).await?;
128 let mut keys = Vec::new();
129
130 for row in rows {
131 keys.push(ApiKey::from_row(&row)?);
132 }
133
134 Ok(keys)
135 }
136}