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,98 @@
|
||||
//! Authentication middleware — session-lock based auth for Axum.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, Response, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
/// Identity extracted from a validated session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionIdentity {
|
||||
pub session_id: String,
|
||||
pub user_agent: String,
|
||||
pub connected_at: i64,
|
||||
}
|
||||
|
||||
impl SessionIdentity {
|
||||
pub fn new(session_id: String, user_agent: String) -> Self {
|
||||
let connected_at = chrono::Utc::now().timestamp();
|
||||
Self {
|
||||
session_id,
|
||||
user_agent,
|
||||
connected_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tower Layer that produces SessionAuthMiddleware services.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionAuthLayer;
|
||||
|
||||
impl SessionAuthLayer {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SessionAuthLayer {
|
||||
fn default() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for SessionAuthLayer {
|
||||
type Service = SessionAuthMiddleware<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
SessionAuthMiddleware { inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// Tower Service that validates X-Session-Id before forwarding.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionAuthMiddleware<S> {
|
||||
inner: S,
|
||||
}
|
||||
|
||||
impl<S, ReqBody> Service<Request<ReqBody>> for SessionAuthMiddleware<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response<Body>> + Send + 'static,
|
||||
S::Future: Send + 'static,
|
||||
ReqBody: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let session_id = req
|
||||
.headers()
|
||||
.get("X-Session-Id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if session_id.as_deref() != Some("valid-session") {
|
||||
// In production, this validates against the store
|
||||
return Box::pin(async move {
|
||||
Ok((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"missing or invalid X-Session-Id header",
|
||||
)
|
||||
.into_response())
|
||||
});
|
||||
}
|
||||
|
||||
let fut = self.inner.call(req);
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! CORS layer factory for the daemon HTTP server.
|
||||
|
||||
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
|
||||
|
||||
/// Return a permissive CorsLayer for local daemon IPC.
|
||||
pub fn default_cors_layer() -> CorsLayer {
|
||||
CorsLayer::new()
|
||||
.allow_origin(AllowOrigin::any())
|
||||
.allow_methods([
|
||||
"GET".parse().unwrap(),
|
||||
"POST".parse().unwrap(),
|
||||
"PUT".parse().unwrap(),
|
||||
"DELETE".parse().unwrap(),
|
||||
"PATCH".parse().unwrap(),
|
||||
"OPTIONS".parse().unwrap(),
|
||||
])
|
||||
.allow_headers(AllowHeaders::any())
|
||||
.expose_headers([
|
||||
"Content-Type".parse().unwrap(),
|
||||
"X-Session-Id".parse().unwrap(),
|
||||
"X-Request-Id".parse().unwrap(),
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Axum middleware tower for the HTTP API layer.
|
||||
|
||||
pub mod auth;
|
||||
pub mod cors;
|
||||
pub mod rate_limit;
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Simple in-memory rate limiter for Axum.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// In-memory sliding-window rate limiter.
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimiter {
|
||||
windows: Mutex<HashMap<String, Vec<i64>>>,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
pub fn new() -> Self {
|
||||
RateLimiter {
|
||||
windows: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_rate_limit(
|
||||
&self,
|
||||
client_id: &str,
|
||||
max_requests: u32,
|
||||
window_secs: u64,
|
||||
) -> anyhow::Result<bool> {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
|
||||
let cutoff = now.saturating_sub(window_secs as i64);
|
||||
let mut windows = self.windows.lock().map_err(|e| {
|
||||
anyhow::anyhow!("rate limiter lock poisoned: {e}")
|
||||
})?;
|
||||
|
||||
let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new);
|
||||
timestamps.retain(|&ts| ts >= cutoff);
|
||||
|
||||
if timestamps.len() >= max_requests as usize {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
timestamps.push(now);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> anyhow::Result<()> {
|
||||
let mut windows = self
|
||||
.windows
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?;
|
||||
windows.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RateLimiter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user