Reworked user portal. devided admin and user UI
Build and Publish / Build and Publish Docker Image (push) Successful in 3m7s

This commit is contained in:
Ultradesu
2026-06-30 17:02:56 +03:00
parent c3db347da6
commit 3def3afeda
8 changed files with 1059 additions and 93 deletions
+123 -24
View File
@@ -22,7 +22,28 @@ use crate::{auth, vpn};
// ---------------------------------------------------------------------------
fn json_error(status: cot::http::StatusCode, message: &str) -> cot::response::Response {
let body = serde_json::json!({ "error": message });
json_error_typed(
status,
status.canonical_reason().unwrap_or("request_failed"),
status.canonical_reason().unwrap_or("Request failed"),
message,
"",
)
}
fn json_error_typed(
status: cot::http::StatusCode,
code: &str,
title: &str,
message: &str,
detail: &str,
) -> cot::response::Response {
let body = serde_json::json!({
"code": code,
"title": title,
"error": message,
"detail": detail,
});
cot::http::Response::builder()
.status(status)
.header(cot::http::header::CONTENT_TYPE, "application/json")
@@ -76,6 +97,7 @@ struct CreateVpnClientRequest {
struct MutateVpnClientResponse {
client: vpn::VpnClientView,
sync: vpn::SecretSyncResult,
notice: Option<ApiNotice>,
}
#[derive(Debug, Deserialize, JsonSchema)]
@@ -86,6 +108,16 @@ struct SetEnabledRequest {
#[derive(Debug, Serialize, JsonSchema)]
struct DeleteVpnClientResponse {
sync: vpn::SecretSyncResult,
notice: Option<ApiNotice>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
struct ApiNotice {
kind: String,
code: String,
title: String,
message: String,
detail: String,
}
#[derive(Debug, Serialize, JsonSchema)]
@@ -154,9 +186,22 @@ async fn vpn_status_handler(
tracing::debug!(user_id = user.id, "VPN rollout status requested");
let (config, _) = AppConfig::load_with_db(&db).await;
let status = vpn::read_rollout_status_from_kubernetes(&config)
.await
.map_err(|e| cot::Error::internal(format!("failed to read VPN rollout status: {e}")))?;
let mut status = match vpn::read_rollout_status_from_kubernetes(&config).await {
Ok(status) => status,
Err(e) => {
return Ok(json_error_typed(
cot::http::StatusCode::SERVICE_UNAVAILABLE,
"vpn_status_unavailable",
"Server status unavailable",
"Could not load VPN server status.",
&e,
));
}
};
if user.role != auth::Role::Admin {
let disabled = vpn::disabled_endpoint_names(&config);
status.pods.retain(|pod| !disabled.contains(&pod.node_name));
}
Json(status).into_response()
}
@@ -177,18 +222,32 @@ async fn create_vpn_client_handler(
};
let (config, _) = AppConfig::load_with_db(&db).await;
let client =
vpn::VpnClient::create_for_owner(&db, user.id, &request.name, &config.vpn_client_cidr)
.await
.map_err(|e| cot::Error::internal(format!("failed to create client: {e}")))?;
let sync = vpn::sync_from_database(&db, &config)
.await
.map_err(|e| cot::Error::internal(format!("failed to sync client Secret: {e}")))?;
let client = match vpn::VpnClient::create_for_owner(
&db,
user.id,
&request.name,
&config.vpn_client_cidr,
)
.await
{
Ok(client) => client,
Err(e) => {
return Ok(json_error_typed(
cot::http::StatusCode::BAD_REQUEST,
"client_create_failed",
"Could not create key",
"The key was not created.",
&e.to_string(),
));
}
};
let (sync, notice) = sync_after_client_mutation(&db, &config).await;
let owner_map = owner_view_map(&db, std::slice::from_ref(&client)).await?;
Json(MutateVpnClientResponse {
client: client_view_with_owner(client, &owner_map),
sync,
notice,
})
.into_response()
}
@@ -221,14 +280,13 @@ async fn set_vpn_client_enabled_handler(
.await
.map_err(|e| cot::Error::internal(format!("failed to update client: {e}")))?;
let (config, _) = AppConfig::load_with_db(&db).await;
let sync = vpn::sync_from_database(&db, &config)
.await
.map_err(|e| cot::Error::internal(format!("failed to sync client Secret: {e}")))?;
let (sync, notice) = sync_after_client_mutation(&db, &config).await;
let owner_map = owner_view_map(&db, std::slice::from_ref(&client)).await?;
Json(MutateVpnClientResponse {
client: client_view_with_owner(client, &owner_map),
sync,
notice,
})
.into_response()
}
@@ -259,11 +317,9 @@ async fn delete_vpn_client_handler(
.await
.map_err(|e| cot::Error::internal(format!("failed to delete client: {e}")))?;
let (config, _) = AppConfig::load_with_db(&db).await;
let sync = vpn::sync_from_database(&db, &config)
.await
.map_err(|e| cot::Error::internal(format!("failed to sync client Secret: {e}")))?;
let (sync, notice) = sync_after_client_mutation(&db, &config).await;
Json(DeleteVpnClientResponse { sync }).into_response()
Json(DeleteVpnClientResponse { sync, notice }).into_response()
}
async fn vpn_client_config_handler(
@@ -289,9 +345,18 @@ async fn vpn_client_config_handler(
};
let (config, _) = AppConfig::load_with_db(&db).await;
let runtime = vpn::read_runtime_from_kubernetes(&config)
.await
.map_err(|e| cot::Error::internal(format!("failed to read VPN runtime: {e}")))?;
let runtime = match vpn::read_runtime_from_kubernetes(&config).await {
Ok(runtime) => runtime,
Err(e) => {
return Ok(json_error_typed(
cot::http::StatusCode::SERVICE_UNAVAILABLE,
"vpn_runtime_unavailable",
"VPN servers are unavailable",
"Could not load VPN server list.",
&e,
));
}
};
let endpoints = vpn::filter_enabled_endpoints(runtime.endpoints, &config);
if endpoints.is_empty() {
return Ok(json_error(
@@ -381,6 +446,31 @@ fn client_view_with_owner(
view
}
async fn sync_after_client_mutation(
db: &Database,
config: &AppConfig,
) -> (vpn::SecretSyncResult, Option<ApiNotice>) {
match vpn::sync_from_database(db, config).await {
Ok(sync) => (sync, None),
Err(e) => {
tracing::warn!(error = %e, "client data changed but Secret sync failed");
(
vpn::SecretSyncResult {
changed: false,
message: "client data saved; Secret sync failed".to_owned(),
},
Some(ApiNotice {
kind: "warning".to_owned(),
code: "secret_sync_failed".to_owned(),
title: "Saved locally".to_owned(),
message: "The key list was updated, but VPN servers did not receive the new config yet.".to_owned(),
detail: e,
}),
)
}
}
}
fn render_qr_svg(value: &str) -> cot::Result<String> {
let code = QrCode::new(value.as_bytes())
.map_err(|e| cot::Error::internal(format!("failed to render QR code: {e}")))?;
@@ -411,9 +501,18 @@ async fn sync_vpn_clients_handler(
);
let (config, _) = AppConfig::load_with_db(&db).await;
let sync = vpn::sync_from_database(&db, &config)
.await
.map_err(|e| cot::Error::internal(format!("failed to sync client Secret: {e}")))?;
let sync = match vpn::sync_from_database(&db, &config).await {
Ok(sync) => sync,
Err(e) => {
return Ok(json_error_typed(
cot::http::StatusCode::SERVICE_UNAVAILABLE,
"secret_sync_failed",
"Secret sync failed",
"Could not apply client config to VPN servers.",
&e,
));
}
};
Json(sync).into_response()
}