use axum::{
Json,
http::{StatusCode, header},
response::{Html, IntoResponse},
};
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::web::auth::AuthenticatedUser;
use crate::channels::web::types::*;
pub async fn index_handler() -> Html<&'static str> {
Html(include_str!("../static/index.html"))
}
pub async fn css_handler() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/css")],
include_str!("../static/style.css"),
)
}
pub async fn js_handler() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "application/javascript")],
include_str!("../static/app.js"),
)
}
pub async fn health_handler() -> Json<HealthResponse> {
Json(HealthResponse {
status: "healthy",
channel: "gateway",
})
}
use axum::extract::Path;
pub async fn project_redirect_handler(Path(project_id): Path<String>) -> impl IntoResponse {
axum::response::Redirect::permanent(&format!("/projects/{project_id}/"))
}
pub async fn project_index_handler(Path(project_id): Path<String>) -> impl IntoResponse {
serve_project_file(&project_id, "index.html").await
}
pub async fn project_file_handler(
Path((project_id, path)): Path<(String, String)>,
) -> impl IntoResponse {
serve_project_file(&project_id, &path).await
}
async fn serve_project_file(project_id: &str, path: &str) -> axum::response::Response {
if project_id.contains('/')
|| project_id.contains('\\')
|| project_id.contains("..")
|| project_id.is_empty()
{
return (StatusCode::BAD_REQUEST, "Invalid project ID").into_response();
}
let base = ironclaw_base_dir().join("projects").join(project_id);
let file_path = base.join(path);
let canonical = match file_path.canonicalize() {
Ok(p) => p,
Err(_) => return (StatusCode::NOT_FOUND, "Not found").into_response(),
};
let base_canonical = match base.canonicalize() {
Ok(p) => p,
Err(_) => return (StatusCode::NOT_FOUND, "Not found").into_response(),
};
if !canonical.starts_with(&base_canonical) {
return (StatusCode::FORBIDDEN, "Forbidden").into_response();
}
match tokio::fs::read(&canonical).await {
Ok(contents) => {
let mime = mime_guess::from_path(&canonical)
.first_or_octet_stream()
.to_string();
([(header::CONTENT_TYPE, mime)], contents).into_response()
}
Err(_) => (StatusCode::NOT_FOUND, "Not found").into_response(),
}
}
use std::convert::Infallible;
use std::sync::Arc;
use axum::extract::State;
use axum::response::sse::{Event, KeepAlive, Sse};
use tokio_stream::StreamExt;
use crate::channels::web::server::GatewayState;
pub async fn logs_events_handler(
State(state): State<Arc<GatewayState>>,
AuthenticatedUser(_user): AuthenticatedUser,
) -> Result<
Sse<impl futures::Stream<Item = Result<Event, Infallible>> + Send + 'static>,
(StatusCode, String),
> {
let broadcaster = state.log_broadcaster.as_ref().ok_or((
StatusCode::SERVICE_UNAVAILABLE,
"Log broadcaster not available".to_string(),
))?;
let rx = broadcaster.subscribe();
let history = broadcaster.recent_entries();
let history_stream = futures::stream::iter(history).map(|entry| {
let data = serde_json::to_string(&entry).unwrap_or_default();
Ok(Event::default().event("log").data(data))
});
let live_stream = tokio_stream::wrappers::BroadcastStream::new(rx)
.filter_map(|result| result.ok())
.map(|entry| {
let data = serde_json::to_string(&entry).unwrap_or_default();
Ok(Event::default().event("log").data(data))
});
let stream = history_stream.chain(live_stream);
Ok(Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(std::time::Duration::from_secs(30))
.text(""),
))
}
pub async fn gateway_status_handler(
State(state): State<Arc<GatewayState>>,
AuthenticatedUser(_user): AuthenticatedUser,
) -> Json<GatewayStatusResponse> {
let sse_connections = state.sse.connection_count();
let ws_connections = state
.ws_tracker
.as_ref()
.map(|t| t.connection_count())
.unwrap_or(0);
Json(GatewayStatusResponse {
sse_connections,
ws_connections,
total_connections: sse_connections + ws_connections,
})
}
#[derive(serde::Serialize)]
pub struct GatewayStatusResponse {
pub sse_connections: u64,
pub ws_connections: u64,
pub total_connections: u64,
}