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,140 @@
|
||||
//! JWT authentication middleware for Axum.
|
||||
//!
|
||||
//! Validates the `Authorization: Bearer <token>` header on every protected
|
||||
//! request. Injects the validated subject claim into request extensions for
|
||||
//! downstream handlers to consume.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Request → JwtAuthLayer → extract Bearer token → verify JWT → inject claims
|
||||
//! → inner service → Response
|
||||
//! ```
|
||||
//!
|
||||
//! If the token is missing, expired, or has an invalid signature the request
|
||||
//! is rejected with 401 Unauthorized before reaching any handler.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, Response, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tower::{Layer, Service};
|
||||
|
||||
use crate::state::ApiState;
|
||||
|
||||
/// Claims extracted from a valid JWT, injected into request extensions.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JwtClaims {
|
||||
/// Subject identifier (username/user ID).
|
||||
pub sub: String,
|
||||
}
|
||||
|
||||
/// Tower Layer that produces `JwtAuthMiddleware` services.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JwtAuthLayer {
|
||||
/// HMAC secret used to verify JWT signatures (reference into `ApiState`).
|
||||
state: Arc<ApiState>,
|
||||
}
|
||||
|
||||
impl JwtAuthLayer {
|
||||
/// Create a new JWT auth layer with the given shared API state.
|
||||
pub fn new(state: Arc<ApiState>) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for JwtAuthLayer {
|
||||
type Service = JwtAuthMiddleware<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
JwtAuthMiddleware {
|
||||
inner,
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tower Service that validates JWT Bearer tokens before forwarding.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JwtAuthMiddleware<S> {
|
||||
inner: S,
|
||||
state: Arc<ApiState>,
|
||||
}
|
||||
|
||||
impl<S, ReqBody> Service<Request<ReqBody>> for JwtAuthMiddleware<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 {
|
||||
// Extract the Authorization header
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let secret = self.state.jwt_secret.clone();
|
||||
|
||||
if let Some(auth_value) = auth_header {
|
||||
// Expect "Bearer <token>"
|
||||
if let Some(token) = auth_value.strip_prefix("Bearer ") {
|
||||
match zesdex_infrastructure::auth::jwt::verify_token(&secret, token) {
|
||||
Ok(claims) => {
|
||||
// Inject claims as extension for downstream handlers
|
||||
let mut req = req;
|
||||
req.extensions_mut().insert(JwtClaims {
|
||||
sub: claims.sub,
|
||||
});
|
||||
let fut = self.inner.call(req);
|
||||
return Box::pin(fut);
|
||||
}
|
||||
Err(e) => {
|
||||
let response = (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": "Invalid token",
|
||||
"detail": e.to_string()
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
return Box::pin(async move { Ok(response) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No valid Authorization header
|
||||
let response = (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "Missing or invalid Authorization header"})),
|
||||
)
|
||||
.into_response();
|
||||
Box::pin(async move { Ok(response) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: check if a request has a valid JWT in its Authorization header.
|
||||
///
|
||||
/// Intended for use in middleware layers or route guards that need quick
|
||||
/// authentication verification without extracting the full claims.
|
||||
pub fn is_authenticated(req: &Request<Body>) -> bool {
|
||||
req.extensions().get::<JwtClaims>().is_some()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Axum middleware layers for the REST API.
|
||||
//!
|
||||
//! Provides tower `Layer` implementations for cross-cutting concerns:
|
||||
//! - `auth` — JWT-based authentication layer
|
||||
|
||||
pub mod auth;
|
||||
Reference in New Issue
Block a user