Reworked user portal. devided admin and user UI
Build and Publish / Build and Publish Docker Image (push) Successful in 3m7s
Build and Publish / Build and Publish Docker Image (push) Successful in 3m7s
This commit is contained in:
+123
-24
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user