Added web ui

This commit is contained in:
Ultradesu
2025-07-18 18:06:26 +03:00
parent d5ce88dfff
commit 3fa43d276d
3 changed files with 95 additions and 61 deletions

65
Cargo.lock generated
View File

@@ -1064,7 +1064,7 @@ dependencies = [
[[package]]
name = "khm"
version = "0.4.2"
version = "0.5.0"
dependencies = [
"actix-web",
"base64 0.21.7",
@@ -1075,6 +1075,7 @@ dependencies = [
"log",
"regex",
"reqwest",
"rust-embed",
"serde",
"serde_json",
"tokio",
@@ -1575,6 +1576,40 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rust-embed"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -1649,6 +1684,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.23"
@@ -2134,6 +2178,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@@ -2258,6 +2312,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@@ -19,3 +19,4 @@ clap = { version = "4", features = ["derive"] }
chrono = "0.4.38"
reqwest = { version = "0.12", features = ["json"] }
hostname = "0.3"
rust-embed = "8.0"

View File

@@ -1,11 +1,23 @@
use actix_web::{web, HttpResponse, Result};
use log::info;
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashSet;
use tokio_postgres::Client;
use crate::server::{get_keys_from_db, Flows};
#[derive(RustEmbed)]
#[folder = "static/"]
struct StaticAssets;
#[derive(Deserialize)]
struct DeleteKeyPath {
flow_id: String,
server: String,
}
// API endpoint to get list of available flows
pub async fn get_flows_api(allowed_flows: web::Data<Vec<String>>) -> Result<HttpResponse> {
info!("API request for available flows");
@@ -15,13 +27,11 @@ pub async fn get_flows_api(allowed_flows: web::Data<Vec<String>>) -> Result<Http
// API endpoint to delete a specific key by server name
pub async fn delete_key_by_server(
flows: web::Data<Flows>,
flow_id: web::Path<String>,
server: web::Path<String>,
path: web::Path<(String, String)>,
db_client: web::Data<std::sync::Arc<Client>>,
allowed_flows: web::Data<Vec<String>>,
) -> Result<HttpResponse> {
let flow_id_str = flow_id.into_inner();
let server_name = server.into_inner();
let (flow_id_str, server_name) = path.into_inner();
info!("API request to delete key for server '{}' in flow '{}'", server_name, flow_id_str);
@@ -130,36 +140,16 @@ async fn delete_key_from_db(
Ok(std::cmp::max(flow_delete_count, total_deleted))
}
// Serve static files
// Serve static files from embedded assets
pub async fn serve_static_file(path: web::Path<String>) -> Result<HttpResponse> {
let file_path = path.into_inner();
// Get the path to the static directory relative to the executable
let exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
let exe_dir = exe_path.parent().unwrap_or_else(|| std::path::Path::new("."));
let static_dir = exe_dir.join("static");
// Fallback to current directory if static dir doesn't exist next to executable
let static_dir = if static_dir.exists() {
static_dir
} else {
std::path::PathBuf::from("static")
};
let full_path = static_dir.join(&file_path);
// Security check - ensure the path is within the static directory
if !full_path.starts_with(&static_dir) {
return Ok(HttpResponse::Forbidden().body("Access denied"));
}
if !full_path.exists() {
return Ok(HttpResponse::NotFound().body(format!("File not found: {}", full_path.display())));
}
match tokio::fs::read(&full_path).await {
Ok(contents) => {
let content_type = match full_path.extension().and_then(|s| s.to_str()) {
match StaticAssets::get(&file_path) {
Some(content) => {
let content_type = match std::path::Path::new(&file_path)
.extension()
.and_then(|s| s.to_str())
{
Some("html") => "text/html; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("js") => "application/javascript; charset=utf-8",
@@ -171,44 +161,24 @@ pub async fn serve_static_file(path: web::Path<String>) -> Result<HttpResponse>
Ok(HttpResponse::Ok()
.content_type(content_type)
.body(contents))
.body(content.data.as_ref().to_vec()))
}
Err(e) => {
info!("Failed to read file {}: {}", full_path.display(), e);
Ok(HttpResponse::InternalServerError().body("Failed to read file"))
None => {
Ok(HttpResponse::NotFound().body(format!("File not found: {}", file_path)))
}
}
}
// Serve the main web interface
// Serve the main web interface from embedded assets
pub async fn serve_web_interface() -> Result<HttpResponse> {
// Get the path to the static directory relative to the executable
let exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
let exe_dir = exe_path.parent().unwrap_or_else(|| std::path::Path::new("."));
let static_dir = exe_dir.join("static");
// Fallback to current directory if static dir doesn't exist next to executable
let static_dir = if static_dir.exists() {
static_dir
} else {
std::path::PathBuf::from("static")
};
let index_path = static_dir.join("index.html");
if !index_path.exists() {
return Ok(HttpResponse::NotFound().body(format!("Web interface not found: {}", index_path.display())));
}
match tokio::fs::read(&index_path).await {
Ok(contents) => {
match StaticAssets::get("index.html") {
Some(content) => {
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(contents))
.body(content.data.as_ref().to_vec()))
}
Err(e) => {
info!("Failed to read index.html: {}", e);
Ok(HttpResponse::InternalServerError().body("Failed to load web interface"))
None => {
Ok(HttpResponse::NotFound().body("Web interface not found"))
}
}
}