Files
zesdex/apps/interfaces/web/src/lib.rs
T
asepharyana 55677dd671 feat: Add SOLID principles and TDD reference documentation
- Created solid.md to document the SOLID principles for clean code practices.
- Created tdd.md to outline Test Driven Development principles and practices.
- Added kana-rust-backend-best-practice.md as a reference guide for building a Rust backend using Axum and SeaORM.
- Established push-flow-convention.md to enforce pre-commit and pre-push hooks with versioning rules.
- Introduced AGENTS.md to provide guidance on best practices and available commands for Kilo.
- Configured kilo.json to include new skills and agents for enhanced functionality.
- Added lefthook.yml for managing git hooks to ensure code quality and adherence to conventions.
2026-07-21 07:10:17 +07:00

112 lines
3.7 KiB
Rust

//! Web frontend interface — serves the browser-based UI.
//!
//! The frontend assets (JS/HTML/CSS) are expected to be built into
//! a `dist/` directory at compile time via `include_dir!`, or served
//! from a path at runtime.
//!
//! ## Development
//!
//! During development, point `--web-dir` to the frontend dev server
//! or build directory.
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use tracing::{info, warn};
/// Web server state.
pub struct WebState {
/// Directory from which to serve static files.
pub static_dir: PathBuf,
}
/// Build the web frontend router.
pub fn build_router(state: Arc<WebState>) -> Router {
Router::new()
.route("/", get(index_handler))
.route("/{*path}", get(static_handler))
.with_state(state)
}
/// Serve the index.html for root requests.
async fn index_handler(
axum::extract::State(state): axum::extract::State<Arc<WebState>>,
) -> Result<Html<String>, StatusCode> {
let index_path = state.static_dir.join("index.html");
match fs::read_to_string(&index_path).await {
Ok(html) => Ok(Html(html)),
Err(_) => {
// Return a minimal HTML page when no frontend is built
Ok(Html(
r#"<!DOCTYPE html>
<html><head><title>Zesdex Web</title>
<meta charset="utf-8">
<style>body{font-family:sans-serif;padding:2em;background:#1a1b26;color:#c0caf5}
h1{color:#7aa2f7}a{color:#bb9af7}</style></head>
<body>
<h1>Zesdex Web</h1>
<p>Web interface is ready.</p>
<p>To connect the frontend:</p>
<ol>
<li>Build the frontend: <code>cd apps/interfaces/web && npm install && npm run build</code></li>
<li>Restart with <code>--web-dir apps/interfaces/web/dist</code></li>
</ol>
</body></html>"#.to_string(),
))
}
}
}
/// Serve static files from the configured directory.
async fn static_handler(
axum::extract::State(state): axum::extract::State<Arc<WebState>>,
path: axum::extract::Path<String>,
) -> Response {
let file_path = state.static_dir.join(path.0);
// Security: prevent directory traversal
let canonical = match file_path.canonicalize() {
Ok(p) => p,
Err(_) => return (StatusCode::NOT_FOUND, "Not found").into_response(),
};
if !canonical.starts_with(&state.static_dir) {
return (StatusCode::FORBIDDEN, "Forbidden").into_response();
}
match fs::read(&canonical).await {
Ok(data) => {
let mime = mime_guess::from_path(&canonical).first_or_octet_stream();
Response::builder()
.status(200)
.header("Content-Type", mime.to_string())
.body(axum::body::Body::from(data))
.expect("Response builder with known-valid status and header")
.into_response()
}
Err(_) => (StatusCode::NOT_FOUND, "Not found").into_response(),
}
}
/// Run the web frontend server.
pub async fn run_server(port: u16, static_dir: Option<PathBuf>) -> anyhow::Result<()> {
let dir = static_dir.unwrap_or_else(|| {
let p = PathBuf::from("apps/interfaces/web/dist");
if p.exists() {
p
} else {
warn!("No static dir found at {:?}, using current dir", p);
PathBuf::from(".")
}
});
let state = Arc::new(WebState { static_dir: dir });
let app = build_router(state);
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
info!("Web frontend server listening on http://{addr}");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}