feat: add service catalog management with policy integration for domain-based resource access control

Add ServiceCatalogItem type and services CRUD API endpoints (list, create, update, delete). Extend Policy type to include services array with domain, upstream_ip, proxy_ip, and ports metadata.

Add ServicesPage component with table view and create/edit modals for managing service definitions. Include service name, domain, proxy, and upstream columns with port parsing logic.

Integrate service selection
This commit is contained in:
2026-03-18 13:09:54 +01:00
parent 0ac93dfeb6
commit 6cf49ff3e0
25 changed files with 1375 additions and 99 deletions

View File

@@ -1,6 +1,15 @@
mod tunnel_manager;
use std::{fs, io::Cursor, net::TcpListener, path::PathBuf, sync::Mutex};
use std::{
fs,
io::Cursor,
net::TcpListener,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
},
};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use png::{ColorType, Decoder};
@@ -22,6 +31,7 @@ const SINGLE_INSTANCE_ADDR: &str = "127.0.0.1:53190";
struct AppState {
session: Mutex<Option<SessionState>>,
tray: Mutex<Option<TrayState>>,
tunnel_action_in_progress: AtomicBool,
single_instance_lock: TcpListener,
}
@@ -392,14 +402,29 @@ async fn select_access_profile(app: AppHandle, profile_id: String) -> Result<Enr
#[tauri::command]
async fn connect_tunnel(app: AppHandle) -> Result<EnrollmentResult, String> {
let session_state = sync_current_session(&app).await?;
let state = app.state::<AppState>();
state.tunnel_action_in_progress.store(true, Ordering::SeqCst);
let session_state = match sync_current_session(&app).await {
Ok(value) => value,
Err(err) => {
state.tunnel_action_in_progress.store(false, Ordering::SeqCst);
return Err(err);
}
};
let app_handle = app.clone();
let profile_path = session_state.profile_path.clone();
let result = tauri::async_runtime::spawn_blocking(move || {
let result = match tauri::async_runtime::spawn_blocking(move || {
tunnel_manager::connect(&app_handle, std::path::Path::new(&profile_path))
})
.await
.map_err(|err| format!("Unable to join tunnel connect task: {err}"))?;
.map_err(|err| format!("Unable to join tunnel connect task: {err}")) {
Ok(value) => value,
Err(err) => {
state.tunnel_action_in_progress.store(false, Ordering::SeqCst);
return Err(err);
}
};
state.tunnel_action_in_progress.store(false, Ordering::SeqCst);
refresh_tray_menu(&app);
result?;
Ok(session_state.enrollment)
@@ -407,39 +432,65 @@ async fn connect_tunnel(app: AppHandle) -> Result<EnrollmentResult, String> {
#[tauri::command]
async fn disconnect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
state.tunnel_action_in_progress.store(true, Ordering::SeqCst);
let profile_path = {
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
session.profile_path.clone()
};
let app_handle = app.clone();
let result = tauri::async_runtime::spawn_blocking(move || {
let result = match tauri::async_runtime::spawn_blocking(move || {
tunnel_manager::disconnect(&app_handle, std::path::Path::new(&profile_path))
})
.await
.map_err(|err| format!("Unable to join tunnel disconnect task: {err}"))?;
.map_err(|err| format!("Unable to join tunnel disconnect task: {err}")) {
Ok(value) => value,
Err(err) => {
state.tunnel_action_in_progress.store(false, Ordering::SeqCst);
return Err(err);
}
};
state.tunnel_action_in_progress.store(false, Ordering::SeqCst);
refresh_tray_menu(&app);
result
}
#[tauri::command]
fn tunnel_status(app: AppHandle, state: State<'_, AppState>) -> Result<bool, String> {
async fn tunnel_status(app: AppHandle, state: State<'_, AppState>) -> Result<bool, String> {
if state.tunnel_action_in_progress.load(Ordering::SeqCst) {
return Ok(false);
}
let profile_path = {
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
session.profile_path.clone()
};
tunnel_manager::is_active(&app, std::path::Path::new(&profile_path))
tauri::async_runtime::spawn_blocking(move || {
tunnel_manager::is_active(&app, std::path::Path::new(&profile_path))
})
.await
.map_err(|err| format!("Unable to join tunnel status task: {err}"))?
}
#[tauri::command]
fn tunnel_metrics(app: AppHandle, state: State<'_, AppState>) -> Result<TunnelMetrics, String> {
async fn tunnel_metrics(app: AppHandle, state: State<'_, AppState>) -> Result<TunnelMetrics, String> {
if state.tunnel_action_in_progress.load(Ordering::SeqCst) {
return Ok(TunnelMetrics {
active: false,
rx_bytes: 0,
tx_bytes: 0,
});
}
let profile_path = {
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
session.profile_path.clone()
};
let metrics = tunnel_manager::metrics(&app, std::path::Path::new(&profile_path))?;
let metrics = tauri::async_runtime::spawn_blocking(move || {
tunnel_manager::metrics(&app, std::path::Path::new(&profile_path))
})
.await
.map_err(|err| format!("Unable to join tunnel metrics task: {err}"))??;
let mapped = TunnelMetrics {
active: metrics.active,
rx_bytes: metrics.rx_bytes,
@@ -542,6 +593,13 @@ fn format_data_size(bytes: u64) -> String {
fn current_metrics(app: &AppHandle) -> Result<TunnelMetrics, String> {
let state = app.state::<AppState>();
if state.tunnel_action_in_progress.load(Ordering::SeqCst) {
return Ok(TunnelMetrics {
active: false,
rx_bytes: 0,
tx_bytes: 0,
});
}
let profile_path = {
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
@@ -923,6 +981,7 @@ pub fn run() {
sent_item,
toggle_item,
})),
tunnel_action_in_progress: AtomicBool::new(false),
single_instance_lock,
});
refresh_tray_menu(app.handle());