init
Some checks failed
CI / ci (push) Failing after 1m48s

This commit is contained in:
2026-08-25 23:24:36 +02:00
commit 95739892de
466 changed files with 33918 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
[package]
name = "auth"
edition.workspace = true
version.workspace = true
[dependencies]
domain.workspace = true
config.workspace = true
async-trait.workspace = true
chrono.workspace = true
uuid.workspace = true
serde.workspace = true
jsonwebtoken.workspace = true
argon2.workspace = true

View File

@@ -0,0 +1,71 @@
use chrono::{Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use config::AuthConfig;
use domain::auth::GeneratedToken;
use domain::errors::DomainError;
use domain::user::UserId;
#[derive(serde::Serialize, serde::Deserialize)]
struct Claims {
sub: String,
exp: u64,
}
pub struct JwtAuthService {
secret: String,
ttl_seconds: i64,
}
impl JwtAuthService {
pub fn new(config: &AuthConfig) -> Result<Self, DomainError> {
let secret = config
.jwt_secret
.clone()
.filter(|s| !s.is_empty())
.ok_or_else(|| DomainError::InvalidInput("JWT secret must be configured".into()))?;
Ok(Self {
secret,
ttl_seconds: config.access_token_ttl_seconds as i64,
})
}
}
#[async_trait::async_trait]
impl domain::ports::AuthServicePort for JwtAuthService {
async fn generate_token(&self, user_id: &UserId) -> Result<GeneratedToken, DomainError> {
let expires_at = Utc::now() + Duration::seconds(self.ttl_seconds);
let claims = Claims {
sub: user_id.value().to_string(),
exp: expires_at.timestamp() as u64,
};
let token = jsonwebtoken::encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(self.secret.as_bytes()),
)
.map_err(|e| DomainError::InvalidInput(format!("failed to generate token: {e}")))?;
Ok(GeneratedToken::new(token, expires_at))
}
async fn validate_token(&self, token: &str) -> Result<UserId, DomainError> {
let data = jsonwebtoken::decode::<Claims>(
token,
&DecodingKey::from_secret(self.secret.as_bytes()),
&Validation::default(),
)
.map_err(|_| DomainError::Unauthorized("invalid or expired token".into()))?;
let uuid: uuid::Uuid = data
.claims
.sub
.parse()
.map_err(|_| DomainError::Unauthorized("invalid token subject".into()))?;
Ok(UserId::from_uuid(uuid))
}
}

View File

@@ -0,0 +1,5 @@
mod jwt_service;
mod password_hasher;
pub use jwt_service::JwtAuthService;
pub use password_hasher::Argon2PasswordHasher;

View File

@@ -0,0 +1,33 @@
use argon2::password_hash::SaltString;
use argon2::password_hash::rand_core::OsRng;
use argon2::{Argon2, PasswordHasher, PasswordVerifier};
use domain::errors::DomainError;
pub struct Argon2PasswordHasher;
impl domain::ports::PasswordHasherPort for Argon2PasswordHasher {
fn hash(&self, raw_password: &str) -> Result<domain::user::PasswordHash, DomainError> {
let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default()
.hash_password(raw_password.as_bytes(), &salt)
.map_err(|e| DomainError::InvalidInput(format!("failed to hash password: {e}")))?
.to_string();
Ok(domain::user::PasswordHash::new(hash))
}
fn verify(
&self,
raw_password: &str,
hash: &domain::user::PasswordHash,
) -> Result<bool, DomainError> {
let parsed = argon2::password_hash::PasswordHash::new(hash.value())
.map_err(|e| DomainError::InvalidInput(format!("invalid password hash: {e}")))?;
Ok(Argon2::default()
.verify_password(raw_password.as_bytes(), &parsed)
.is_ok())
}
}