From 0222a168dbebe4fe7eadb57ab7d187aab25995b5 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 16 May 2026 11:03:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(auth):=20add=20ApiKeyServiceImpl=20?= =?UTF-8?q?=E2=80=94=20moves=20sha256=20hashing=20out=20of=20presentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/adapters/auth/Cargo.toml | 2 + crates/adapters/auth/src/api_key_service.rs | 89 +++++++++++++++++++++ crates/adapters/auth/src/lib.rs | 4 + 3 files changed, 95 insertions(+) create mode 100644 crates/adapters/auth/src/api_key_service.rs diff --git a/crates/adapters/auth/Cargo.toml b/crates/adapters/auth/Cargo.toml index 58f56d0..eb10094 100644 --- a/crates/adapters/auth/Cargo.toml +++ b/crates/adapters/auth/Cargo.toml @@ -15,3 +15,5 @@ jsonwebtoken = "9" argon2 = "0.5" bcrypt = "0.15" rand = "0.8" +sha2 = "0.10" +hex = "0.4" diff --git a/crates/adapters/auth/src/api_key_service.rs b/crates/adapters/auth/src/api_key_service.rs new file mode 100644 index 0000000..7622396 --- /dev/null +++ b/crates/adapters/auth/src/api_key_service.rs @@ -0,0 +1,89 @@ +use async_trait::async_trait; +use domain::{ + errors::DomainError, + ports::{ApiKeyRepository, ApiKeyService}, + value_objects::UserId, +}; +use sha2::{Digest, Sha256}; +use std::sync::Arc; + +pub struct ApiKeyServiceImpl { + repo: Arc, +} + +impl ApiKeyServiceImpl { + pub fn new(repo: Arc) -> Self { + Self { repo } + } + + fn hash(raw: &str) -> String { + hex::encode(Sha256::digest(raw.as_bytes())) + } +} + +#[async_trait] +impl ApiKeyService for ApiKeyServiceImpl { + async fn validate_key(&self, raw_key: &str) -> Result, DomainError> { + let hash = Self::hash(raw_key); + Ok(self.repo.find_by_hash(&hash).await?.map(|k| k.user_id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use chrono::Utc; + use domain::{ + errors::DomainError, + models::api_key::ApiKey, + ports::ApiKeyRepository, + value_objects::{ApiKeyId, UserId}, + }; + use std::sync::{Arc, Mutex}; + + struct FakeApiKeyRepo(Mutex>); + + #[async_trait] + impl ApiKeyRepository for FakeApiKeyRepo { + async fn save(&self, key: &ApiKey) -> Result<(), DomainError> { + self.0.lock().unwrap().push(key.clone()); + Ok(()) + } + async fn find_by_hash(&self, hash: &str) -> Result, DomainError> { + Ok(self.0.lock().unwrap().iter().find(|k| k.key_hash == hash).cloned()) + } + async fn list_for_user(&self, _uid: &UserId) -> Result, DomainError> { + Ok(vec![]) + } + async fn delete(&self, _id: &ApiKeyId, _uid: &UserId) -> Result<(), DomainError> { + Ok(()) + } + } + + #[tokio::test] + async fn validate_known_key_returns_user_id() { + let uid = UserId::new(); + let raw = "super-secret-key"; + let hash = ApiKeyServiceImpl::hash(raw); + let key = ApiKey { + id: ApiKeyId::new(), + user_id: uid.clone(), + key_hash: hash, + name: "test".into(), + created_at: Utc::now(), + }; + let repo = Arc::new(FakeApiKeyRepo(Mutex::new(vec![key]))); + let svc = ApiKeyServiceImpl::new(repo); + let result = svc.validate_key(raw).await.unwrap(); + assert_eq!(result.unwrap().as_uuid(), uid.as_uuid()); + } + + #[tokio::test] + async fn validate_unknown_key_returns_none() { + let repo = Arc::new(FakeApiKeyRepo(Mutex::new(vec![]))); + let svc = ApiKeyServiceImpl::new(repo); + let result = svc.validate_key("unknown-key").await.unwrap(); + assert!(result.is_none()); + } +} diff --git a/crates/adapters/auth/src/lib.rs b/crates/adapters/auth/src/lib.rs index 8d51f68..c4e240a 100644 --- a/crates/adapters/auth/src/lib.rs +++ b/crates/adapters/auth/src/lib.rs @@ -1,3 +1,5 @@ +mod api_key_service; + use async_trait::async_trait; use chrono::{Duration, Utc}; use domain::{ @@ -8,6 +10,8 @@ use domain::{ use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; +pub use api_key_service::ApiKeyServiceImpl; + #[derive(Serialize, Deserialize)] struct Claims { sub: String,