feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1

Merged
GKaszewski merged 334 commits from v2 into master 2026-05-16 09:42:43 +00:00
3 changed files with 95 additions and 0 deletions
Showing only changes of commit 0222a168db - Show all commits

View File

@@ -15,3 +15,5 @@ jsonwebtoken = "9"
argon2 = "0.5"
bcrypt = "0.15"
rand = "0.8"
sha2 = "0.10"
hex = "0.4"

View 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());
}
}

View File

@@ -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,