use oxidite_db::sqlx::{self, FromRow};
use sha2::{Sha256, Digest};
use rand::Rng;
use base64::Engine;
#[derive(FromRow, Clone, Debug)]
pub struct ApiKey {
pub id: i64,
pub user_id: i64,
pub key_hash: String,
pub name: String,
pub last_used_at: Option<i64>,
pub expires_at: Option<i64>,
pub created_at: i64,
pub updated_at: i64,
}
impl ApiKey {
pub fn generate_key() -> String {
let mut rng = rand::rng();
let random_bytes: Vec<u8> = (0..32).map(|_| rng.random()).collect();
let key = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(&random_bytes);
format!("ox_{}", key)
}
pub fn hash_key(key: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(key.as_bytes());
format!("{:x}", hasher.finalize())
}
pub async fn create_for_user<D: oxidite_db::Database>(
db: &D,
user_id: i64,
name: &str,
expires_at: Option<i64>,
) -> oxidite_db::Result<(ApiKey, String)> {
let key = Self::generate_key();
let key_hash = Self::hash_key(&key);
let now = chrono::Utc::now().timestamp();
let query = oxidite_db::sqlx::query(
"INSERT INTO api_keys (user_id, key_hash, name, expires_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)"
)
.bind(user_id)
.bind(&key_hash)
.bind(name)
.bind(expires_at)
.bind(now)
.bind(now);
db.execute_query(query).await?;
let get_query = oxidite_db::sqlx::query(
"SELECT * FROM api_keys WHERE key_hash = ?"
)
.bind(&key_hash);
let row = db.fetch_one(get_query).await?
.ok_or_else(|| sqlx::Error::RowNotFound)?;
let api_key = ApiKey::from_row(&row)?;
Ok((api_key, key))
}
pub async fn verify_key<D: oxidite_db::Database + ?Sized>(
db: &D,
key: &str,
) -> oxidite_db::Result<Option<ApiKey>> {
let key_hash = Self::hash_key(key);
let now = chrono::Utc::now().timestamp();
let query = oxidite_db::sqlx::query(
"SELECT * FROM api_keys
WHERE key_hash = ?
AND (expires_at IS NULL OR expires_at > ?)"
)
.bind(&key_hash)
.bind(now);
let row = db.fetch_one(query).await?;
match row {
Some(row) => {
let mut api_key = ApiKey::from_row(&row)?;
let update_query = oxidite_db::sqlx::query(
"UPDATE api_keys SET last_used_at = ? WHERE id = ?"
)
.bind(now)
.bind(api_key.id);
let _ = db.execute_query(update_query).await;
api_key.last_used_at = Some(now);
Ok(Some(api_key))
}
None => Ok(None),
}
}
pub async fn revoke<D: oxidite_db::Database>(
db: &D,
key_id: i64,
user_id: i64,
) -> oxidite_db::Result<bool> {
let query = oxidite_db::sqlx::query(
"DELETE FROM api_keys WHERE id = ? AND user_id = ?"
)
.bind(key_id)
.bind(user_id);
let rows = db.execute_query(query).await?;
Ok(rows > 0)
}
pub async fn get_user_keys<D: oxidite_db::Database>(
db: &D,
user_id: i64,
) -> oxidite_db::Result<Vec<ApiKey>> {
let query = oxidite_db::sqlx::query(
"SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at DESC"
)
.bind(user_id);
let rows = db.fetch_all(query).await?;
let mut keys = Vec::new();
for row in rows {
keys.push(ApiKey::from_row(&row)?);
}
Ok(keys)
}
}