Skip to main content

oxidite_auth/
api_key.rs

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    /// Generate a new API key with prefix
20    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    /// Hash an API key for storage
29    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    /// Create a new API key for a user
36    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        // Retrieve the created key
60        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    /// Find API key by key string and verify it's valid
72    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                // Update last_used_at
94                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    /// Revoke (delete) an API key
109    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    /// Get all API keys for a user
124    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}