//! 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>` 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> + 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> + 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) -> 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 { 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 { 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` 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>, /// 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, llm_api_key: impl Into, llm_model: impl Into, llm_base_url: Option, ) -> 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, } } }