feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks
feat(tui): implement status bar with connection and turn state indicators feat(tui): create workflow panel for agent status and progress visualization feat(web): introduce web frontend interface with static file serving feat(ws): add WebSocket interface for real-time communication and session management
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
//! 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 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};
|
||||
|
||||
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, 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, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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,
|
||||
|
||||
/// LLM provider client for chat completions.
|
||||
pub llm_client: zesdex_infrastructure::llm::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 llm_client = zesdex_infrastructure::llm::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,
|
||||
llm_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user