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]] [[package]]
name = "khm" name = "khm"
version = "0.4.2" version = "0.5.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"base64 0.21.7", "base64 0.21.7",
@@ -1075,6 +1075,7 @@ dependencies = [
"log", "log",
"regex", "regex",
"reqwest", "reqwest",
"rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -1575,6 +1576,40 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@@ -1649,6 +1684,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 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]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.23" version = "0.1.23"
@@ -2134,6 +2178,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 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]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -2258,6 +2312,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 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]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

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

View File

@@ -1,11 +1,23 @@
use actix_web::{web, HttpResponse, Result}; use actix_web::{web, HttpResponse, Result};
use log::info; use log::info;
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::collections::HashSet; use std::collections::HashSet;
use tokio_postgres::Client; use tokio_postgres::Client;
use crate::server::{get_keys_from_db, Flows}; 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 // API endpoint to get list of available flows
pub async fn get_flows_api(allowed_flows: web::Data<Vec<String>>) -> Result<HttpResponse> { pub async fn get_flows_api(allowed_flows: web::Data<Vec<String>>) -> Result<HttpResponse> {
info!("API request for available flows"); 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 // API endpoint to delete a specific key by server name
pub async fn delete_key_by_server( pub async fn delete_key_by_server(
flows: web::Data<Flows>, flows: web::Data<Flows>,
flow_id: web::Path<String>, path: web::Path<(String, String)>,
server: web::Path<String>,
db_client: web::Data<std::sync::Arc<Client>>, db_client: web::Data<std::sync::Arc<Client>>,
allowed_flows: web::Data<Vec<String>>, allowed_flows: web::Data<Vec<String>>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let flow_id_str = flow_id.into_inner(); let (flow_id_str, server_name) = path.into_inner();
let server_name = server.into_inner();
info!("API request to delete key for server '{}' in flow '{}'", server_name, flow_id_str); 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)) 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> { pub async fn serve_static_file(path: web::Path<String>) -> Result<HttpResponse> {
let file_path = path.into_inner(); let file_path = path.into_inner();
// Get the path to the static directory relative to the executable match StaticAssets::get(&file_path) {
let exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from(".")); Some(content) => {
let exe_dir = exe_path.parent().unwrap_or_else(|| std::path::Path::new(".")); let content_type = match std::path::Path::new(&file_path)
let static_dir = exe_dir.join("static"); .extension()
.and_then(|s| s.to_str())
// 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()) {
Some("html") => "text/html; charset=utf-8", Some("html") => "text/html; charset=utf-8",
Some("css") => "text/css; charset=utf-8", Some("css") => "text/css; charset=utf-8",
Some("js") => "application/javascript; 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() Ok(HttpResponse::Ok()
.content_type(content_type) .content_type(content_type)
.body(contents)) .body(content.data.as_ref().to_vec()))
} }
Err(e) => { None => {
info!("Failed to read file {}: {}", full_path.display(), e); Ok(HttpResponse::NotFound().body(format!("File not found: {}", file_path)))
Ok(HttpResponse::InternalServerError().body("Failed to read file"))
} }
} }
} }
// Serve the main web interface // Serve the main web interface from embedded assets
pub async fn serve_web_interface() -> Result<HttpResponse> { pub async fn serve_web_interface() -> Result<HttpResponse> {
// Get the path to the static directory relative to the executable match StaticAssets::get("index.html") {
let exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from(".")); Some(content) => {
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) => {
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(contents)) .body(content.data.as_ref().to_vec()))
} }
Err(e) => { None => {
info!("Failed to read index.html: {}", e); Ok(HttpResponse::NotFound().body("Web interface not found"))
Ok(HttpResponse::InternalServerError().body("Failed to load web interface"))
} }
} }
} }