feat(auth): add ApiKeyServiceImpl — moves sha256 hashing out of presentation
This commit is contained in:
@@ -15,3 +15,5 @@ jsonwebtoken = "9"
|
|||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
bcrypt = "0.15"
|
bcrypt = "0.15"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
|||||||
89
crates/adapters/auth/src/api_key_service.rs
Normal file
89
crates/adapters/auth/src/api_key_service.rs
Normal file
@@ -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<dyn ApiKeyRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiKeyServiceImpl {
|
||||||
|
pub fn new(repo: Arc<dyn ApiKeyRepository>) -> 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<Option<UserId>, 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<Vec<ApiKey>>);
|
||||||
|
|
||||||
|
#[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<Option<ApiKey>, DomainError> {
|
||||||
|
Ok(self.0.lock().unwrap().iter().find(|k| k.key_hash == hash).cloned())
|
||||||
|
}
|
||||||
|
async fn list_for_user(&self, _uid: &UserId) -> Result<Vec<ApiKey>, 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
mod api_key_service;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use domain::{
|
use domain::{
|
||||||
@@ -8,6 +10,8 @@ use domain::{
|
|||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use api_key_service::ApiKeyServiceImpl;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
sub: String,
|
sub: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user