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 = 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        // Retrieve the created key
57        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    /// Find API key by key string and verify it's valid
69    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                // Update last_used_at
90                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    /// Revoke (delete) an API key
104    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    /// Get all API keys for a user
118    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}