use async_trait::async_trait;
use chrono::{DateTime, Utc};
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use subtle::ConstantTimeEq;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::errors::AppError;
pub const API_KEY_PREFIX: &str = "ck_";
#[derive(Debug, Clone)]
pub struct ApiKeyEntity {
pub id: Uuid,
pub user_id: Uuid,
pub key_hash: String,
pub key_prefix: String,
pub label: String,
pub created_at: DateTime<Utc>,
pub last_used_at: Option<DateTime<Utc>>,
}
impl ApiKeyEntity {
pub fn new(user_id: Uuid, raw_key: &str, label: &str) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
user_id,
key_hash: hash_api_key(raw_key),
key_prefix: raw_key.chars().take(16).collect(),
label: label.to_string(),
created_at: now,
last_used_at: None,
}
}
}
pub fn generate_api_key() -> String {
let suffix: String = OsRng
.sample_iter(&Alphanumeric)
.take(43)
.map(char::from)
.collect();
format!("{}{}", API_KEY_PREFIX, suffix)
}
pub fn hash_api_key(key: &str) -> String {
let hash = Sha256::digest(key.as_bytes());
hex::encode(hash)
}
#[async_trait]
pub trait ApiKeyRepository: Send + Sync {
async fn create(&self, entity: ApiKeyEntity) -> Result<ApiKeyEntity, AppError>;
async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<ApiKeyEntity>, AppError>;
async fn find_one_by_user_id(&self, user_id: Uuid) -> Result<Option<ApiKeyEntity>, AppError>;
async fn find_by_key(&self, raw_key: &str) -> Result<Option<ApiKeyEntity>, AppError>;
async fn delete_for_user(&self, user_id: Uuid) -> Result<(), AppError>;
async fn delete_by_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, AppError>;
async fn update_last_used(&self, id: Uuid) -> Result<(), AppError>;
}
pub struct InMemoryApiKeyRepository {
keys: RwLock<HashMap<Uuid, ApiKeyEntity>>,
}
impl InMemoryApiKeyRepository {
pub fn new() -> Self {
Self {
keys: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryApiKeyRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ApiKeyRepository for InMemoryApiKeyRepository {
async fn create(&self, entity: ApiKeyEntity) -> Result<ApiKeyEntity, AppError> {
let mut keys = self.keys.write().await;
let duplicate = keys
.values()
.any(|k| k.user_id == entity.user_id && k.label == entity.label);
if duplicate {
return Err(AppError::Validation(format!(
"API key with label '{}' already exists",
entity.label
)));
}
keys.insert(entity.id, entity.clone());
Ok(entity)
}
async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<ApiKeyEntity>, AppError> {
let keys = self.keys.read().await;
let mut result: Vec<_> = keys
.values()
.filter(|k| k.user_id == user_id)
.cloned()
.collect();
result.sort_by_key(|k| k.created_at);
Ok(result)
}
async fn find_one_by_user_id(&self, user_id: Uuid) -> Result<Option<ApiKeyEntity>, AppError> {
let keys = self.keys.read().await;
Ok(keys.values().find(|k| k.user_id == user_id).cloned())
}
async fn find_by_key(&self, raw_key: &str) -> Result<Option<ApiKeyEntity>, AppError> {
let keys = self.keys.read().await;
let key_hash = hash_api_key(raw_key);
Ok(keys
.values()
.find(|k| k.key_hash.as_bytes().ct_eq(key_hash.as_bytes()).into())
.cloned())
}
async fn delete_for_user(&self, user_id: Uuid) -> Result<(), AppError> {
let mut keys = self.keys.write().await;
keys.retain(|_, k| k.user_id != user_id);
Ok(())
}
async fn delete_by_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, AppError> {
let mut keys = self.keys.write().await;
let before = keys.len();
keys.retain(|_, k| !(k.id == id && k.user_id == user_id));
Ok(keys.len() < before)
}
async fn update_last_used(&self, id: Uuid) -> Result<(), AppError> {
let mut keys = self.keys.write().await;
if let Some(entity) = keys.get_mut(&id) {
entity.last_used_at = Some(Utc::now());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_api_key() {
let key = generate_api_key();
assert!(key.starts_with(API_KEY_PREFIX));
assert_eq!(key.len(), API_KEY_PREFIX.len() + 43);
}
#[test]
fn test_hash_api_key() {
let key = "ck_abc123";
let hash1 = hash_api_key(key);
let hash2 = hash_api_key(key);
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 64); }
#[tokio::test]
async fn test_create_and_find_by_user() {
let repo = InMemoryApiKeyRepository::new();
let user_id = Uuid::new_v4();
let raw_key = generate_api_key();
let entity = ApiKeyEntity::new(user_id, &raw_key, "default");
repo.create(entity).await.unwrap();
let found = repo.find_by_user_id(user_id).await.unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].user_id, user_id);
assert_eq!(found[0].label, "default");
}
#[tokio::test]
async fn test_find_by_key() {
let repo = InMemoryApiKeyRepository::new();
let user_id = Uuid::new_v4();
let raw_key = generate_api_key();
let entity = ApiKeyEntity::new(user_id, &raw_key, "default");
repo.create(entity).await.unwrap();
let found = repo.find_by_key(&raw_key).await.unwrap();
assert!(found.is_some());
let invalid = repo.find_by_key("ck_invalid_key_123").await.unwrap();
assert!(invalid.is_none());
}
#[tokio::test]
async fn test_delete_for_user() {
let repo = InMemoryApiKeyRepository::new();
let user_id = Uuid::new_v4();
let raw_key = generate_api_key();
let entity = ApiKeyEntity::new(user_id, &raw_key, "default");
repo.create(entity).await.unwrap();
assert!(!repo.find_by_user_id(user_id).await.unwrap().is_empty());
repo.delete_for_user(user_id).await.unwrap();
assert!(repo.find_by_user_id(user_id).await.unwrap().is_empty());
}
#[tokio::test]
async fn test_delete_by_id() {
let repo = InMemoryApiKeyRepository::new();
let user_id = Uuid::new_v4();
let raw_key = generate_api_key();
let entity = ApiKeyEntity::new(user_id, &raw_key, "default");
let id = entity.id;
repo.create(entity).await.unwrap();
assert!(!repo.delete_by_id(id, Uuid::new_v4()).await.unwrap());
assert!(!repo.find_by_user_id(user_id).await.unwrap().is_empty());
assert!(repo.delete_by_id(id, user_id).await.unwrap());
assert!(repo.find_by_user_id(user_id).await.unwrap().is_empty());
}
#[tokio::test]
async fn test_update_last_used() {
let repo = InMemoryApiKeyRepository::new();
let user_id = Uuid::new_v4();
let raw_key = generate_api_key();
let entity = ApiKeyEntity::new(user_id, &raw_key, "default");
let id = entity.id;
repo.create(entity).await.unwrap();
let before = repo.find_one_by_user_id(user_id).await.unwrap().unwrap();
assert!(before.last_used_at.is_none());
repo.update_last_used(id).await.unwrap();
let after = repo.find_one_by_user_id(user_id).await.unwrap().unwrap();
assert!(after.last_used_at.is_some());
}
#[tokio::test]
async fn test_multi_key_per_user() {
let repo = InMemoryApiKeyRepository::new();
let user_id = Uuid::new_v4();
let key1 = generate_api_key();
let key2 = generate_api_key();
repo.create(ApiKeyEntity::new(user_id, &key1, "default"))
.await
.unwrap();
repo.create(ApiKeyEntity::new(user_id, &key2, "bot-alpha"))
.await
.unwrap();
let keys = repo.find_by_user_id(user_id).await.unwrap();
assert_eq!(keys.len(), 2);
let key3 = generate_api_key();
let result = repo
.create(ApiKeyEntity::new(user_id, &key3, "default"))
.await;
assert!(result.is_err());
}
}