- Tambah users_lock (Mutex) di ApiState untuk serialisasi read-modify-write users.json pada endpoint register; lock hanya dipegang selama operasi file sinkron (tidak pernah lintas .await, menjaga future tetap Send) - Hash password dihitung sebelum lock sehingga request concurrent tidak saling blokir selama hashing Argon2
318 lines
12 KiB
Rust
318 lines
12 KiB
Rust
//! Shared application state for the REST API server.
|
|
//!
|
|
//! `ApiState` holds concrete service implementations wired to infrastructure
|
|
//! adapters. It is constructed at the composition root and shared across all
|
|
//! handlers via Axum's `State` extractor (wrapped in `Arc`).
|
|
//!
|
|
//! # Flow
|
|
//!
|
|
//! 1. `ApiState::new(base_dir)` creates all services with their concrete repos.
|
|
//! 2. `build_router()` wraps it in `Arc` and passes it to the Axum `Router`.
|
|
//! 3. Handlers extract `State<Arc<ApiState>>` and delegate to the services.
|
|
//!
|
|
//! # Port trait implementations
|
|
//!
|
|
//! This module also provides simple wrapper types that implement the
|
|
//! application-layer port traits using infrastructure functions:
|
|
//!
|
|
//! - `Argon2PasswordService` — implements `PasswordService` via
|
|
//! `infrastructure::auth::password`
|
|
//! - `JwtTokenService` — implements `TokenService` via
|
|
//! `infrastructure::auth::jwt`
|
|
|
|
use std::fmt;
|
|
use std::future::Future;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use zesdex_application::ports::{PasswordService, TokenService};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Port trait implementations (wrap infrastructure free functions)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Password-hashing service backed by Argon2id (infrastructure).
|
|
///
|
|
/// Delegates to `zesdex_infrastructure::auth::password::{hash_password, verify_password}`.
|
|
#[derive(Clone)]
|
|
pub struct Argon2PasswordService;
|
|
|
|
impl fmt::Debug for Argon2PasswordService {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("Argon2PasswordService").finish()
|
|
}
|
|
}
|
|
|
|
impl PasswordService for Argon2PasswordService {
|
|
/// Hash a plaintext password using Argon2id with a random salt.
|
|
fn hash(&self, password: &str) -> impl Future<Output = anyhow::Result<String>> + Send {
|
|
zesdex_infrastructure::auth::password::hash_password(password)
|
|
}
|
|
|
|
/// Verify a plaintext password against a stored PHC string.
|
|
fn verify(
|
|
&self,
|
|
password: &str,
|
|
hash: &str,
|
|
) -> impl Future<Output = anyhow::Result<bool>> + Send {
|
|
zesdex_infrastructure::auth::password::verify_password(password, hash)
|
|
}
|
|
}
|
|
|
|
/// JWT token service backed by HS256 (infrastructure).
|
|
///
|
|
/// Delegates to `zesdex_infrastructure::auth::jwt::{create_token, verify_token}`.
|
|
#[derive(Clone)]
|
|
pub struct JwtTokenService {
|
|
/// HMAC secret key used for signing and verification.
|
|
pub secret: String,
|
|
/// Token expiry in seconds (default: 3600 = 1 hour).
|
|
pub access_token_expiry_secs: u64,
|
|
/// Refresh token expiry in seconds (default: 604800 = 7 days).
|
|
pub refresh_token_expiry_secs: u64,
|
|
}
|
|
|
|
impl fmt::Debug for JwtTokenService {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("JwtTokenService")
|
|
.field("access_token_expiry_secs", &self.access_token_expiry_secs)
|
|
.field("refresh_token_expiry_secs", &self.refresh_token_expiry_secs)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl JwtTokenService {
|
|
/// Create a new JWT service with the given HMAC secret.
|
|
pub fn new(secret: impl Into<String>) -> Self {
|
|
JwtTokenService {
|
|
secret: secret.into(),
|
|
access_token_expiry_secs: 3600,
|
|
refresh_token_expiry_secs: 604800,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TokenService for JwtTokenService {
|
|
/// Generate an access + refresh token pair for the given subject.
|
|
fn generate_tokens(&self, sub: &str) -> anyhow::Result<(String, String)> {
|
|
use zesdex_infrastructure::auth::jwt::{create_token, JwtClaims, TokenType};
|
|
|
|
let now = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs();
|
|
|
|
// Access token
|
|
let access_claims = JwtClaims::new(
|
|
sub.to_string(),
|
|
now + self.access_token_expiry_secs,
|
|
TokenType::Access,
|
|
None,
|
|
);
|
|
let access_token = create_token(&self.secret, access_claims)?;
|
|
|
|
// Refresh token (longer-lived)
|
|
let refresh_claims = JwtClaims::new(
|
|
sub.to_string(),
|
|
now + self.refresh_token_expiry_secs,
|
|
TokenType::Refresh,
|
|
None,
|
|
);
|
|
let refresh_token = create_token(&self.secret, refresh_claims)?;
|
|
|
|
Ok((access_token, refresh_token))
|
|
}
|
|
|
|
/// Verify an access token and return the subject claim.
|
|
fn verify_access_token(&self, token: &str) -> anyhow::Result<String> {
|
|
use zesdex_infrastructure::auth::jwt::verify_token;
|
|
|
|
let claims = verify_token(&self.secret, token)?;
|
|
Ok(claims.sub)
|
|
}
|
|
|
|
/// Verify a refresh token and return the subject claim.
|
|
///
|
|
/// Enforces that the presented token is a **refresh** token (`typ =
|
|
/// "refresh"`) — an access token presented here is rejected, closing
|
|
/// the replay-window escalation where a stolen 1-hour access token
|
|
/// could otherwise be exchanged for a fresh 7-day credential.
|
|
fn verify_refresh_token(&self, token: &str) -> anyhow::Result<String> {
|
|
use zesdex_infrastructure::auth::jwt::{verify_token, TokenType};
|
|
|
|
let claims = verify_token(&self.secret, token)?;
|
|
if claims.token_type != TokenType::Refresh {
|
|
anyhow::bail!("token is not a refresh token");
|
|
}
|
|
Ok(claims.sub)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ApiState
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Shared application state for the REST API server.
|
|
///
|
|
/// Holds all service implementations, repository instances, and configuration
|
|
/// needed by the HTTP handlers. Constructed once at startup and shared
|
|
/// across all requests via `Arc<ApiState>`.
|
|
///
|
|
/// `ApiState` does NOT derive `Clone` or `Debug` because the inner service
|
|
/// types may not implement those traits. It is always wrapped in `Arc`.
|
|
pub struct ApiState {
|
|
/// Base directory for all Zesdex data stores (sessions, settings, etc.).
|
|
pub store_base_dir: PathBuf,
|
|
/// JWT secret key for token signing/verification.
|
|
pub jwt_secret: String,
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Service implementations (application-layer use cases)
|
|
// -----------------------------------------------------------------------
|
|
|
|
/// Session lifecycle management (create, list, archive).
|
|
pub session_service:
|
|
zesdex_application::auth::SessionServiceImpl<
|
|
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository,
|
|
zesdex_infrastructure::persistence::iam::session_lock_repo::FileSystemSessionLockRepository,
|
|
>,
|
|
|
|
/// Conversation message history CRUD.
|
|
pub conversation_service:
|
|
zesdex_application::cms::ConversationServiceImpl<
|
|
zesdex_infrastructure::persistence::cms::conversation_repo::JsonConversationRepository,
|
|
>,
|
|
|
|
/// Settings load/save.
|
|
pub settings_service:
|
|
zesdex_application::cms::SettingsServiceImpl<
|
|
zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository,
|
|
zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository,
|
|
>,
|
|
|
|
/// Long-term memory CRUD.
|
|
pub memory_service:
|
|
zesdex_application::cms::MemoryServiceImpl<
|
|
zesdex_infrastructure::persistence::cms::memory_repo::MarkdownMemoryRepository,
|
|
>,
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Port trait implementations (infrastructure wrappers)
|
|
// -----------------------------------------------------------------------
|
|
|
|
/// Argon2id password hashing and verification.
|
|
pub password_service: Argon2PasswordService,
|
|
|
|
/// HS256 JWT token generation and verification.
|
|
pub token_service: JwtTokenService,
|
|
|
|
/// Shared sliding-window limiter for auth endpoints (login/register/refresh).
|
|
pub auth_rate_limiter: zesdex_infrastructure::middleware::rate_limit::RateLimiter,
|
|
|
|
/// Serializes read-modify-write of `users.json` so concurrent register
|
|
/// requests cannot lose writes (TOCTOU race).
|
|
pub users_lock: Arc<std::sync::Mutex<()>>,
|
|
|
|
/// LLM provider client for chat completions.
|
|
pub llm_client: zesdex_infrastructure::llm::provider::LlmClient,
|
|
}
|
|
|
|
impl fmt::Debug for ApiState {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("ApiState")
|
|
.field("store_base_dir", &self.store_base_dir)
|
|
.field("jwt_secret", &"**redacted**")
|
|
.field("session_service", &"SessionServiceImpl { .. }")
|
|
.field("conversation_service", &"ConversationServiceImpl { .. }")
|
|
.field("settings_service", &"SettingsServiceImpl { .. }")
|
|
.field("memory_service", &"MemoryServiceImpl { .. }")
|
|
.field("password_service", &self.password_service)
|
|
.field("token_service", &self.token_service)
|
|
.field("llm_client", &"LlmClient { .. }")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl ApiState {
|
|
/// Construct a new API state with all services wired to their default
|
|
/// infrastructure implementations.
|
|
///
|
|
/// ## Arguments
|
|
/// * `base_dir` — the Zesdex data store root directory (sessions, settings, etc.)
|
|
/// * `jwt_secret` — HMAC secret for JWT signing/verification
|
|
/// * `llm_api_key` — API key for the LLM provider
|
|
/// * `llm_model` — model identifier string
|
|
/// * `llm_base_url` — optional custom API base URL
|
|
///
|
|
/// ## Flow
|
|
///
|
|
/// Creates concrete repository instances → wraps them in application-layer
|
|
/// service implementations → stores everything in `ApiState`.
|
|
pub fn new(
|
|
base_dir: PathBuf,
|
|
jwt_secret: impl Into<String>,
|
|
llm_api_key: impl Into<String>,
|
|
llm_model: impl Into<String>,
|
|
llm_base_url: Option<String>,
|
|
) -> Self {
|
|
let jwt_secret = jwt_secret.into();
|
|
let sessions_dir = base_dir.join("sessions");
|
|
let memory_dir = base_dir.join("memories");
|
|
|
|
// IAM repositories
|
|
let session_repo =
|
|
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository;
|
|
let lock_repo =
|
|
zesdex_infrastructure::persistence::iam::session_lock_repo::FileSystemSessionLockRepository;
|
|
|
|
// CMS repositories
|
|
let conversation_repo =
|
|
zesdex_infrastructure::persistence::cms::conversation_repo::JsonConversationRepository;
|
|
let settings_repo =
|
|
zesdex_infrastructure::persistence::cms::settings_repo::JsonSettingsRepository;
|
|
let app_config_repo =
|
|
zesdex_infrastructure::persistence::cms::app_config_repo::JsonAppConfigRepository;
|
|
let memory_repo =
|
|
zesdex_infrastructure::persistence::cms::memory_repo::MarkdownMemoryRepository;
|
|
|
|
// Application-layer services
|
|
let session_service = zesdex_application::auth::SessionServiceImpl::new(
|
|
session_repo,
|
|
lock_repo,
|
|
base_dir.clone(),
|
|
);
|
|
let conversation_service =
|
|
zesdex_application::cms::ConversationServiceImpl::new(conversation_repo, sessions_dir);
|
|
let settings_service = zesdex_application::cms::SettingsServiceImpl::new(
|
|
settings_repo,
|
|
app_config_repo,
|
|
base_dir.clone(),
|
|
);
|
|
let memory_service =
|
|
zesdex_application::cms::MemoryServiceImpl::new(memory_repo, memory_dir);
|
|
|
|
let token_service = JwtTokenService::new(&jwt_secret);
|
|
let auth_rate_limiter = zesdex_infrastructure::middleware::rate_limit::RateLimiter::new();
|
|
let users_lock = Arc::new(std::sync::Mutex::new(()));
|
|
let llm_client = zesdex_infrastructure::llm::provider::LlmClient::new(
|
|
llm_api_key.into(),
|
|
llm_model.into(),
|
|
llm_base_url,
|
|
);
|
|
|
|
ApiState {
|
|
store_base_dir: base_dir,
|
|
jwt_secret,
|
|
session_service,
|
|
conversation_service,
|
|
settings_service,
|
|
memory_service,
|
|
password_service: Argon2PasswordService,
|
|
token_service,
|
|
auth_rate_limiter,
|
|
users_lock,
|
|
llm_client,
|
|
}
|
|
}
|
|
}
|