feat(token): add refresh token verification to TokenService
feat(bootstrap): create temporary settings and config files to prevent data loss refactor(edit_log): switch from Vec to VecDeque for efficient memory management fix(gateway): ensure store directories are created before starting the API server refactor(bgbash): implement a global singleton for BashControl feat(auth): enhance session authentication middleware to use SessionRepository fix(edit_log_repo): update to use VecDeque for in-memory edit log storage fix(memory_repo): add newline escaping for frontmatter fields fix(session_lock_repo): improve error handling for lock file operations fix(bash_tools): prevent path traversal in job_id argument refactor(delete): enforce empty directory deletion in file system tools fix(edit): optimize string replacement to only replace the first occurrence fix(git_cred): improve credential management with piped input to git commands feat(git_operator): add safety filter to block destructive git operations fix(shell): register background jobs in Bash control feat(spawn): add access tier specification for pipeline stages refactor(hive_mind): run directives concurrently for improved performance fix(auth): update refresh token verification in the refresh handler fix(chat): optimize LLM client usage based on model matching fix(conversations): enhance message deletion to target specific indices feat(api): add JWT authentication middleware for all API routes fix(state): implement refresh token verification in JwtTokenService fix(daemon): improve usage tracking with saturating addition fix(tui): handle compacted messages in the TUI state management
This commit is contained in:
@@ -203,7 +203,7 @@ pub async fn refresh_handler(
|
||||
// Verify the refresh token and extract the subject
|
||||
let sub = state
|
||||
.token_service
|
||||
.verify_access_token(&req.refresh_token)
|
||||
.verify_refresh_token(&req.refresh_token)
|
||||
.map_err(|_| ApiError::Unauthorized("Invalid or expired refresh token".into()))?;
|
||||
|
||||
// Generate a fresh token pair
|
||||
|
||||
@@ -109,18 +109,21 @@ pub async fn chat_completions_handler(
|
||||
|
||||
// Call the LLM provider (non-streaming)
|
||||
//
|
||||
// We create a temporary LlmClient with the overridden model so we
|
||||
// don't mutate the shared state's client.
|
||||
// When the requested model matches the shared client we reuse it to
|
||||
// avoid allocating a new HTTP connection. Otherwise we create a
|
||||
// temporary LlmClient with the requested model — the ownership
|
||||
// lives on the stack via `temp_client`.
|
||||
#[allow(unused_assignments)]
|
||||
let mut temp_client: Option<zesdex_infrastructure::llm::LlmClient> = None;
|
||||
let llm_client = if model == state.llm_client.model {
|
||||
// Use the shared client directly
|
||||
&state.llm_client
|
||||
} else {
|
||||
// Create a modified client for this request (only borrows, but
|
||||
// we need to own it for the call — handled below)
|
||||
//
|
||||
// For simplicity, use the shared client with its model. A full
|
||||
// implementation would override the model per request.
|
||||
&state.llm_client
|
||||
temp_client = Some(zesdex_infrastructure::llm::LlmClient::new(
|
||||
state.llm_client.api_key.clone(),
|
||||
model,
|
||||
Some(state.llm_client.base_url.clone()),
|
||||
));
|
||||
temp_client.as_ref().unwrap()
|
||||
};
|
||||
|
||||
let (response, usage) = llm_client
|
||||
|
||||
@@ -103,29 +103,44 @@ pub async fn add_message_handler(
|
||||
))
|
||||
}
|
||||
|
||||
/// DELETE /sessions/:id/conversations/:cid — delete a message from a conversation.
|
||||
/// DELETE /sessions/:id/conversations/:cid — delete a single message by index.
|
||||
///
|
||||
/// Note: the `cid` parameter currently identifies the message index or the
|
||||
/// entire conversation. For simplicity, this deletes the entire conversation
|
||||
/// and creates a fresh one. A more sophisticated implementation would remove
|
||||
/// a single message by index.
|
||||
/// ## Flow
|
||||
///
|
||||
/// 1. Extract session ID and message index from the path.
|
||||
/// 2. Load the conversation.
|
||||
/// 3. Remove the message at `cid` (zero-based index).
|
||||
/// 4. Persist the updated conversation.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - `404 Not Found` — conversation not found.
|
||||
/// - `400 Bad Request` — session ID is empty or `cid` is not a valid integer.
|
||||
/// - `404 Not Found` — conversation or message index not found.
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn delete_message_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path((id, _cid)): Path<(String, String)>,
|
||||
Path((id, cid)): Path<(String, String)>,
|
||||
) -> Result<axum::http::StatusCode, ApiError> {
|
||||
if id.is_empty() {
|
||||
return Err(ApiError::BadRequest("Session ID is required".into()));
|
||||
}
|
||||
|
||||
// Load conversation and clear all messages
|
||||
// Parse the message index from the path
|
||||
let index: usize = cid
|
||||
.parse()
|
||||
.map_err(|_| ApiError::BadRequest(format!("Invalid message index: {cid}")))?;
|
||||
|
||||
// Load conversation and remove the specific message by index
|
||||
let mut conversation = state.conversation_service.load_conversation(&id)?;
|
||||
|
||||
conversation.messages.clear();
|
||||
if index >= conversation.messages.len() {
|
||||
return Err(ApiError::NotFound(format!(
|
||||
"Message index {index} out of bounds (max: {})",
|
||||
conversation.messages.len().saturating_sub(1)
|
||||
)));
|
||||
}
|
||||
|
||||
conversation.messages.remove(index);
|
||||
state
|
||||
.conversation_service
|
||||
.save_conversation(&conversation)?;
|
||||
|
||||
@@ -56,10 +56,17 @@ pub fn build_router(state: ApiState) -> Router {
|
||||
// CORS layer — permissive for local daemon / development use
|
||||
let cors = CorsLayer::permissive();
|
||||
|
||||
// JWT auth middleware — validates Bearer tokens on all API routes.
|
||||
// Health and auth endpoints (login/register/refresh) are also
|
||||
// protected; adjust route ordering or add an allow-list inside the
|
||||
// middleware if public access is needed.
|
||||
let jwt_auth = middleware::auth::JwtAuthLayer::new(shared_state.clone());
|
||||
|
||||
// Combine all sub-routers under a versioned prefix
|
||||
Router::new()
|
||||
.nest("/api/v1", api_v1_router())
|
||||
.layer(cors)
|
||||
.layer(jwt_auth)
|
||||
.with_state(shared_state)
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,19 @@ impl TokenService for JwtTokenService {
|
||||
let claims = verify_token(&self.secret, token)?;
|
||||
Ok(claims.sub)
|
||||
}
|
||||
|
||||
/// Verify a refresh token and return the subject claim.
|
||||
///
|
||||
/// Delegates to the same JWT verification function as access tokens;
|
||||
/// the signature algorithm and secret are shared. Expiry validation
|
||||
/// is handled by the JWT library against the `exp` claim embedded
|
||||
/// in the token payload.
|
||||
fn verify_refresh_token(&self, token: &str) -> anyhow::Result<String> {
|
||||
use zesdex_infrastructure::auth::jwt::verify_token;
|
||||
|
||||
let claims = verify_token(&self.secret, token)?;
|
||||
Ok(claims.sub)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user