From 89446f210eb8f36565c02dd5394719b3a41bfbc4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Feb 2026 08:54:35 +0100 Subject: [PATCH 1/8] proxy delete API, frontend --- crates/defguard_core/src/handlers/proxy.rs | 50 +++++++++++++++++++++ crates/defguard_core/src/lib.rs | 7 ++- web/messages/en/edge.json | 4 +- web/src/pages/EditEdgePage/EditEdgePage.tsx | 4 ++ web/src/routeTree.gen.ts | 32 +++++++------ 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 6ae43d593d..9b0fc570ec 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -114,3 +114,53 @@ pub(crate) async fn update_proxy( Ok(ApiResponse::json(proxy, StatusCode::OK)) } + +#[utoipa::path( + delete, + path = "/api/v1/proxy/{proxy_id}", + request_body = Proxy, + responses( + (status = 200, description = "Successfully deleted edge.", body = ProxyUpdateData), + (status = 401, description = "Unauthorized to delete edge.", body = ApiResponse, example = json!({"msg": "Session is required"})), + (status = 403, description = "You don't have permission delete an edge.", body = ApiResponse, example = json!({"msg": "access denied"})), + (status = 404, description = "Edge not found", body = ApiResponse, example = json!({"msg": "proxy not found"})), + (status = 500, description = "Unable to delete edge.", body = ApiResponse, example = json!({"msg": "Internal server error"})) + ), + security( + ("cookie" = []), + ("api_token" = []) + ) +)] +pub(crate) async fn delete_proxy( + _role: AdminRole, + Path(proxy_id): Path, + State(appstate): State, + session: SessionInfo, + context: ApiRequestContext, +) -> ApiResult { + debug!("User {} deleteing proxy {proxy_id}", session.user.username); + let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; + + let Some(proxy) = proxy else { + warn!("Proxy {proxy_id} not found"); + return Ok(ApiResponse { + json: Value::Null, + status: StatusCode::NOT_FOUND, + }); + }; + + proxy.delete(&appstate.pool).await?; + + info!("User {} deleted proxy {proxy_id}", session.user.username); + + // TODO(jck) ProxyDeleted event + // appstate.emit_event(ApiEvent { + // context, + // event: Box::new(ApiEventType::ProxyModified { + // before, + // after: proxy.clone(), + // }), + // })?; + + Ok(ApiResponse::default()) +} diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index b542ead1b0..5c6c62a9bb 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -132,7 +132,7 @@ use crate::{ authorization, discovery_keys, openid_configuration, secure_authorization, token, userinfo, }, - proxy::{proxy_details, update_proxy}, + proxy::{delete_proxy, proxy_details, update_proxy}, settings::{ get_settings, get_settings_essentials, patch_settings, set_default_branding, test_ldap_settings, update_settings, @@ -361,7 +361,10 @@ pub fn build_webapp( // Certificate authority .route("/ca", post(create_ca)) // Proxy routes - .route("/proxy/{proxy_id}", get(proxy_details).put(update_proxy)) + .route( + "/proxy/{proxy_id}", + get(proxy_details).put(update_proxy).delete(delete_proxy), + ) // Proxy setup with SSE .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json index b58877883f..0407050bda 100644 --- a/web/messages/en/edge.json +++ b/web/messages/en/edge.json @@ -9,5 +9,7 @@ "edge_edit_public_address": "Public domain", "edge_edit_delete": "Delete", "edge_edit_success": "Edge component updated", - "edge_edit_failed": "Failed to update edge component" + "edge_edit_failed": "Failed to update edge component", + "edge_delete_success": "Edge component deleted", + "edge_delete_failed": "Failed to delete edge component" } diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 7c39c4659f..d83fe85812 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -74,6 +74,10 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { to: '/edge', replace: true, }); + Snackbar.success(m.edge_delete_success()); + }, + onError: () => { + Snackbar.error(m.edge_delete_failed()); }, }); diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index c0cb54e137..421695fee1 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -328,6 +328,7 @@ const AuthorizedDefaultEdgeEdgeIdEditRoute = export interface FileRoutesByFullPath { '/404': typeof R404Route + '/': typeof AuthorizedDefaultRouteWithChildren '/auth': typeof AuthRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute @@ -367,15 +368,16 @@ export interface FileRoutesByFullPath { '/settings/smtp': typeof AuthorizedDefaultSettingsSmtpRoute '/user/$username': typeof AuthorizedDefaultUserUsernameRoute '/vpn-overview/$locationId': typeof AuthorizedDefaultVpnOverviewLocationIdRoute - '/edge': typeof AuthorizedDefaultEdgeIndexRoute - '/locations': typeof AuthorizedDefaultLocationsIndexRoute - '/settings': typeof AuthorizedDefaultSettingsIndexRoute - '/vpn-overview': typeof AuthorizedDefaultVpnOverviewIndexRoute + '/edge/': typeof AuthorizedDefaultEdgeIndexRoute + '/locations/': typeof AuthorizedDefaultLocationsIndexRoute + '/settings/': typeof AuthorizedDefaultSettingsIndexRoute + '/vpn-overview/': typeof AuthorizedDefaultVpnOverviewIndexRoute '/edge/$edgeId/edit': typeof AuthorizedDefaultEdgeEdgeIdEditRoute '/locations/$locationId/edit': typeof AuthorizedDefaultLocationsLocationIdEditRoute } export interface FileRoutesByTo { '/404': typeof R404Route + '/': typeof AuthorizedDefaultRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute '/snackbar': typeof SnackbarRoute @@ -476,6 +478,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/404' + | '/' | '/auth' | '/consent' | '/playground' @@ -515,15 +518,16 @@ export interface FileRouteTypes { | '/settings/smtp' | '/user/$username' | '/vpn-overview/$locationId' - | '/edge' - | '/locations' - | '/settings' - | '/vpn-overview' + | '/edge/' + | '/locations/' + | '/settings/' + | '/vpn-overview/' | '/edge/$edgeId/edit' | '/locations/$locationId/edit' fileRoutesByTo: FileRoutesByTo to: | '/404' + | '/' | '/consent' | '/playground' | '/snackbar' @@ -662,7 +666,7 @@ declare module '@tanstack/react-router' { '/_authorized': { id: '/_authorized' path: '' - fullPath: '' + fullPath: '/' preLoaderRoute: typeof AuthorizedRouteImport parentRoute: typeof rootRouteImport } @@ -711,7 +715,7 @@ declare module '@tanstack/react-router' { '/_authorized/_default': { id: '/_authorized/_default' path: '' - fullPath: '' + fullPath: '/' preLoaderRoute: typeof AuthorizedDefaultRouteImport parentRoute: typeof AuthorizedRoute } @@ -823,28 +827,28 @@ declare module '@tanstack/react-router' { '/_authorized/_default/vpn-overview/': { id: '/_authorized/_default/vpn-overview/' path: '/vpn-overview' - fullPath: '/vpn-overview' + fullPath: '/vpn-overview/' preLoaderRoute: typeof AuthorizedDefaultVpnOverviewIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/settings/': { id: '/_authorized/_default/settings/' path: '/settings' - fullPath: '/settings' + fullPath: '/settings/' preLoaderRoute: typeof AuthorizedDefaultSettingsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/locations/': { id: '/_authorized/_default/locations/' path: '/locations' - fullPath: '/locations' + fullPath: '/locations/' preLoaderRoute: typeof AuthorizedDefaultLocationsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/edge/': { id: '/_authorized/_default/edge/' path: '/edge' - fullPath: '/edge' + fullPath: '/edge/' preLoaderRoute: typeof AuthorizedDefaultEdgeIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } From 645dbebb25bddfb7214e36ff5e9acac8de34950e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 07:43:31 +0100 Subject: [PATCH 2/8] disconnect deleted proxy --- crates/defguard_core/src/handlers/proxy.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 9b0fc570ec..43b7e20b9d 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -2,7 +2,7 @@ use axum::{ Json, extract::{Path, State}, }; -use defguard_common::db::models::proxy::Proxy; +use defguard_common::{db::models::proxy::Proxy, types::proxy::ProxyControlMessage}; use reqwest::StatusCode; use serde_json::{Value, json}; use utoipa::ToSchema; @@ -10,6 +10,7 @@ use utoipa::ToSchema; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, + error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult}, }; @@ -149,11 +150,24 @@ pub(crate) async fn delete_proxy( }); }; + // Disconnect the proxy + appstate + .proxy_control_tx + .send(ProxyControlMessage::ShutdownConnection(proxy.id)) + .await + .map_err(|err| { + error!("Error sending proxy control message: {err:?}"); + WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + // TODO + // 1. Add proxy cert to CRL + // 2. Remove cert files on deleted proxy proxy.delete(&appstate.pool).await?; info!("User {} deleted proxy {proxy_id}", session.user.username); - // TODO(jck) ProxyDeleted event + // TODO(jck) ProxyDeleted event // appstate.emit_event(ApiEvent { // context, // event: Box::new(ApiEventType::ProxyModified { From 7192d750e5ca3c8fef757f175bf01a9eea07aa2f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 07:51:43 +0100 Subject: [PATCH 3/8] ProxyDeleted event --- .../src/db/models/activity_log/metadata.rs | 5 +++++ crates/defguard_core/src/events.rs | 3 +++ crates/defguard_core/src/handlers/proxy.rs | 16 +++++++--------- .../defguard_event_logger/src/description.rs | 3 +++ crates/defguard_event_logger/src/lib.rs | 19 +++++-------------- crates/defguard_event_logger/src/message.rs | 3 +++ .../defguard_event_router/src/handlers/api.rs | 4 ++++ 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/crates/defguard_core/src/db/models/activity_log/metadata.rs b/crates/defguard_core/src/db/models/activity_log/metadata.rs index 406c821b95..3d13c68c94 100644 --- a/crates/defguard_core/src/db/models/activity_log/metadata.rs +++ b/crates/defguard_core/src/db/models/activity_log/metadata.rs @@ -571,3 +571,8 @@ pub struct ProxyModifiedMetadata { pub before: Proxy, pub after: Proxy, } + +#[derive(Serialize)] +pub struct ProxyDeletedMetadata { + pub proxy: Proxy, +} diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index bcf102322f..c4602c1931 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -301,6 +301,9 @@ pub enum ApiEventType { before: Proxy, after: Proxy, }, + ProxyDeleted { + proxy: Proxy, + }, } /// Events from Web API diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 43b7e20b9d..b57b627f3e 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -163,18 +163,16 @@ pub(crate) async fn delete_proxy( // TODO // 1. Add proxy cert to CRL // 2. Remove cert files on deleted proxy - proxy.delete(&appstate.pool).await?; + proxy.clone().delete(&appstate.pool).await?; info!("User {} deleted proxy {proxy_id}", session.user.username); - // TODO(jck) ProxyDeleted event - // appstate.emit_event(ApiEvent { - // context, - // event: Box::new(ApiEventType::ProxyModified { - // before, - // after: proxy.clone(), - // }), - // })?; + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ProxyDeleted { + proxy, + }), + })?; Ok(ApiResponse::default()) } diff --git a/crates/defguard_event_logger/src/description.rs b/crates/defguard_event_logger/src/description.rs index 2cebdcafbd..b9dae26bab 100644 --- a/crates/defguard_event_logger/src/description.rs +++ b/crates/defguard_event_logger/src/description.rs @@ -260,6 +260,9 @@ pub fn get_defguard_event_description(event: &DefguardEvent) -> Option { DefguardEvent::ProxyModified { before: _, after } => { Some(format!("Modified proxy {after}")) } + DefguardEvent::ProxyDeleted { proxy } => { + Some(format!("Deleted proxy {proxy}")) + } } } diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 500c5c41d7..350cc86720 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -3,20 +3,7 @@ use defguard_common::db::NoId; use defguard_core::db::models::activity_log::{ ActivityLogEvent, ActivityLogModule, EventType, metadata::{ - ActivityLogStreamMetadata, ActivityLogStreamModifiedMetadata, ApiTokenMetadata, - ApiTokenRenamedMetadata, AuthenticationKeyMetadata, AuthenticationKeyRenamedMetadata, - ClientConfigurationTokenMetadata, DeviceMetadata, DeviceModifiedMetadata, - EnrollmentDeviceAddedMetadata, EnrollmentTokenMetadata, GroupAssignedMetadata, - GroupMembersModifiedMetadata, GroupMetadata, GroupModifiedMetadata, - GroupsBulkAssignedMetadata, LoginFailedMetadata, MfaLoginFailedMetadata, MfaLoginMetadata, - MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, - OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, - OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata, - ProxyModifiedMetadata, SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, - UserMfaDisabledMetadata, UserModifiedMetadata, UserSnatBindingMetadata, - UserSnatBindingModifiedMetadata, VpnClientMetadata, VpnClientMfaFailedMetadata, - VpnClientMfaMetadata, VpnLocationMetadata, VpnLocationModifiedMetadata, WebHookMetadata, - WebHookModifiedMetadata, WebHookStateChangedMetadata, + ActivityLogStreamMetadata, ActivityLogStreamModifiedMetadata, ApiTokenMetadata, ApiTokenRenamedMetadata, AuthenticationKeyMetadata, AuthenticationKeyRenamedMetadata, ClientConfigurationTokenMetadata, DeviceMetadata, DeviceModifiedMetadata, EnrollmentDeviceAddedMetadata, EnrollmentTokenMetadata, GroupAssignedMetadata, GroupMembersModifiedMetadata, GroupMetadata, GroupModifiedMetadata, GroupsBulkAssignedMetadata, LoginFailedMetadata, MfaLoginFailedMetadata, MfaLoginMetadata, MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata, ProxyDeletedMetadata, ProxyModifiedMetadata, SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, UserMfaDisabledMetadata, UserModifiedMetadata, UserSnatBindingMetadata, UserSnatBindingModifiedMetadata, VpnClientMetadata, VpnClientMfaFailedMetadata, VpnClientMfaMetadata, VpnLocationMetadata, VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata, WebHookStateChangedMetadata }, }; use description::{ @@ -472,6 +459,10 @@ pub async fn run_event_logger( EventType::ProxyModified, serde_json::to_value(ProxyModifiedMetadata { before, after }).ok(), ), + DefguardEvent::ProxyDeleted { proxy } => ( + EventType::ProxyModified, + serde_json::to_value(ProxyDeletedMetadata { proxy }).ok(), + ), }; (module, event_type, description, metadata) } diff --git a/crates/defguard_event_logger/src/message.rs b/crates/defguard_event_logger/src/message.rs index 2421e40cc8..afb0c5a2aa 100644 --- a/crates/defguard_event_logger/src/message.rs +++ b/crates/defguard_event_logger/src/message.rs @@ -341,6 +341,9 @@ pub enum DefguardEvent { before: Proxy, after: Proxy, }, + ProxyDeleted { + proxy: Proxy, + }, } /// Represents activity log events related to client applications diff --git a/crates/defguard_event_router/src/handlers/api.rs b/crates/defguard_event_router/src/handlers/api.rs index d4208b29bd..d8ebe1644b 100644 --- a/crates/defguard_event_router/src/handlers/api.rs +++ b/crates/defguard_event_router/src/handlers/api.rs @@ -393,6 +393,10 @@ impl EventRouter { LoggerEvent::Defguard(Box::new(DefguardEvent::ProxyModified { before, after })), None, ), + ApiEventType::ProxyDeleted { proxy } => ( + LoggerEvent::Defguard(Box::new(DefguardEvent::ProxyDeleted { proxy })), + None, + ), }; self.log_event( EventContext::from_api_context(event.context, location), From c793e6df3403e55ce5eae6c3fe633aebca52d01f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 07:52:14 +0100 Subject: [PATCH 4/8] cargo fmt --- crates/defguard_core/src/handlers/proxy.rs | 12 +++++------- crates/defguard_event_logger/src/description.rs | 4 +--- crates/defguard_event_logger/src/lib.rs | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index b57b627f3e..44666eb74f 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -150,7 +150,7 @@ pub(crate) async fn delete_proxy( }); }; - // Disconnect the proxy + // Disconnect the proxy appstate .proxy_control_tx .send(ProxyControlMessage::ShutdownConnection(proxy.id)) @@ -160,18 +160,16 @@ pub(crate) async fn delete_proxy( WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) })?; - // TODO - // 1. Add proxy cert to CRL - // 2. Remove cert files on deleted proxy + // TODO + // 1. Add proxy cert to CRL + // 2. Remove cert files on deleted proxy proxy.clone().delete(&appstate.pool).await?; info!("User {} deleted proxy {proxy_id}", session.user.username); appstate.emit_event(ApiEvent { context, - event: Box::new(ApiEventType::ProxyDeleted { - proxy, - }), + event: Box::new(ApiEventType::ProxyDeleted { proxy }), })?; Ok(ApiResponse::default()) diff --git a/crates/defguard_event_logger/src/description.rs b/crates/defguard_event_logger/src/description.rs index b9dae26bab..427ee6a12e 100644 --- a/crates/defguard_event_logger/src/description.rs +++ b/crates/defguard_event_logger/src/description.rs @@ -260,9 +260,7 @@ pub fn get_defguard_event_description(event: &DefguardEvent) -> Option { DefguardEvent::ProxyModified { before: _, after } => { Some(format!("Modified proxy {after}")) } - DefguardEvent::ProxyDeleted { proxy } => { - Some(format!("Deleted proxy {proxy}")) - } + DefguardEvent::ProxyDeleted { proxy } => Some(format!("Deleted proxy {proxy}")), } } diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 350cc86720..bf789b2017 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -3,7 +3,21 @@ use defguard_common::db::NoId; use defguard_core::db::models::activity_log::{ ActivityLogEvent, ActivityLogModule, EventType, metadata::{ - ActivityLogStreamMetadata, ActivityLogStreamModifiedMetadata, ApiTokenMetadata, ApiTokenRenamedMetadata, AuthenticationKeyMetadata, AuthenticationKeyRenamedMetadata, ClientConfigurationTokenMetadata, DeviceMetadata, DeviceModifiedMetadata, EnrollmentDeviceAddedMetadata, EnrollmentTokenMetadata, GroupAssignedMetadata, GroupMembersModifiedMetadata, GroupMetadata, GroupModifiedMetadata, GroupsBulkAssignedMetadata, LoginFailedMetadata, MfaLoginFailedMetadata, MfaLoginMetadata, MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata, ProxyDeletedMetadata, ProxyModifiedMetadata, SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, UserMfaDisabledMetadata, UserModifiedMetadata, UserSnatBindingMetadata, UserSnatBindingModifiedMetadata, VpnClientMetadata, VpnClientMfaFailedMetadata, VpnClientMfaMetadata, VpnLocationMetadata, VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata, WebHookStateChangedMetadata + ActivityLogStreamMetadata, ActivityLogStreamModifiedMetadata, ApiTokenMetadata, + ApiTokenRenamedMetadata, AuthenticationKeyMetadata, AuthenticationKeyRenamedMetadata, + ClientConfigurationTokenMetadata, DeviceMetadata, DeviceModifiedMetadata, + EnrollmentDeviceAddedMetadata, EnrollmentTokenMetadata, GroupAssignedMetadata, + GroupMembersModifiedMetadata, GroupMetadata, GroupModifiedMetadata, + GroupsBulkAssignedMetadata, LoginFailedMetadata, MfaLoginFailedMetadata, MfaLoginMetadata, + MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, + OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, + OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata, + ProxyDeletedMetadata, ProxyModifiedMetadata, SettingsUpdateMetadata, + UserGroupsModifiedMetadata, UserMetadata, UserMfaDisabledMetadata, UserModifiedMetadata, + UserSnatBindingMetadata, UserSnatBindingModifiedMetadata, VpnClientMetadata, + VpnClientMfaFailedMetadata, VpnClientMfaMetadata, VpnLocationMetadata, + VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata, + WebHookStateChangedMetadata, }, }; use description::{ From 5f690a1512ef3f3666029c3e74c5f387a706fecd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 08:06:26 +0100 Subject: [PATCH 5/8] proxy delete test --- crates/defguard_core/src/handlers/proxy.rs | 13 ++-- .../tests/integration/api/proxy.rs | 65 ++++++++++++++++++- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 44666eb74f..d4545cc55d 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -10,7 +10,6 @@ use utoipa::ToSchema; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, - error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult}, }; @@ -151,14 +150,16 @@ pub(crate) async fn delete_proxy( }; // Disconnect the proxy - appstate + if let Err(err) = appstate .proxy_control_tx .send(ProxyControlMessage::ShutdownConnection(proxy.id)) .await - .map_err(|err| { - error!("Error sending proxy control message: {err:?}"); - WebError::Http(StatusCode::INTERNAL_SERVER_ERROR) - })?; + { + error!( + "Error shutting down proxy {}, it may be disconnected: {err:?}", + proxy.id + ); + } // TODO // 1. Add proxy cert to CRL diff --git a/crates/defguard_core/tests/integration/api/proxy.rs b/crates/defguard_core/tests/integration/api/proxy.rs index ba4ba23c85..312be5e6ff 100644 --- a/crates/defguard_core/tests/integration/api/proxy.rs +++ b/crates/defguard_core/tests/integration/api/proxy.rs @@ -5,8 +5,38 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::common::{make_test_client, setup_pool}; + +#[sqlx::test] +async fn test_proxy_details(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, _) = make_test_client(pool.clone()).await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new proxy. + let proxy = Proxy::new("test", "localhost", 50051, "public.net") + .save(&pool) + .await + .unwrap(); + + // Get proxy via API + let response = client + .get(format!("/api/v1/proxy/{}", proxy.id)) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Verify proxy is correct + let proxy_from_api: Proxy = response.json().await; + assert_eq!(proxy, proxy_from_api); +} + #[sqlx::test] -async fn test_update_proxy(_: PgPoolOptions, options: PgConnectOptions) { +async fn test_proxy_update(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let (client, _) = make_test_client(pool.clone()).await; @@ -53,3 +83,36 @@ async fn test_update_proxy(_: PgPoolOptions, options: PgConnectOptions) { let proxy_updated: Proxy = response.json().await; assert_eq!(proxy_before_mods, proxy_updated); } + +#[sqlx::test] +async fn test_delete_proxy(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, _) = make_test_client(pool.clone()).await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new proxy. + let proxy = Proxy::new("test", "localhost", 50051, "public.net") + .save(&pool) + .await + .unwrap(); + + // Delete proxy via API + let response = client + .delete(format!("/api/v1/proxy/{}", proxy.id)) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Verify proxy is deleted + let response = client + .get(format!("/api/v1/proxy/{}", proxy.id)) + .send() + .await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(Proxy::all(&pool).await.unwrap().len(), 0); +} From 35706fa67607d3178fc862f4140656f6fbce7d16 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 08:08:59 +0100 Subject: [PATCH 6/8] cargo fmt --- crates/defguard_core/tests/integration/api/proxy.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/proxy.rs b/crates/defguard_core/tests/integration/api/proxy.rs index 312be5e6ff..0cead7ec97 100644 --- a/crates/defguard_core/tests/integration/api/proxy.rs +++ b/crates/defguard_core/tests/integration/api/proxy.rs @@ -5,7 +5,6 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::common::{make_test_client, setup_pool}; - #[sqlx::test] async fn test_proxy_details(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; @@ -23,7 +22,7 @@ async fn test_proxy_details(_: PgPoolOptions, options: PgConnectOptions) { .await .unwrap(); - // Get proxy via API + // Get proxy via API let response = client .get(format!("/api/v1/proxy/{}", proxy.id)) .send() @@ -101,18 +100,18 @@ async fn test_delete_proxy(_: PgPoolOptions, options: PgConnectOptions) { .await .unwrap(); - // Delete proxy via API + // Delete proxy via API let response = client .delete(format!("/api/v1/proxy/{}", proxy.id)) .send() .await; assert_eq!(response.status(), StatusCode::OK); - // Verify proxy is deleted + // Verify proxy is deleted let response = client .get(format!("/api/v1/proxy/{}", proxy.id)) .send() .await; assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert_eq!(Proxy::all(&pool).await.unwrap().len(), 0); + assert_eq!(Proxy::all(&pool).await.unwrap().len(), 0); } From 14bf8b4d795471c09fc441ff2a01b5c6ce94d2bd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 08:11:49 +0100 Subject: [PATCH 7/8] fix utoipa scheme --- crates/defguard_core/src/handlers/proxy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index d4545cc55d..30f8b5f096 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -120,7 +120,7 @@ pub(crate) async fn update_proxy( path = "/api/v1/proxy/{proxy_id}", request_body = Proxy, responses( - (status = 200, description = "Successfully deleted edge.", body = ProxyUpdateData), + (status = 200, description = "Successfully deleted edge.", body = ApiResponse), (status = 401, description = "Unauthorized to delete edge.", body = ApiResponse, example = json!({"msg": "Session is required"})), (status = 403, description = "You don't have permission delete an edge.", body = ApiResponse, example = json!({"msg": "access denied"})), (status = 404, description = "Edge not found", body = ApiResponse, example = json!({"msg": "proxy not found"})), From af7f18d985964c76bef3c9d89fcc0815ea701354 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 3 Feb 2026 09:43:53 +0100 Subject: [PATCH 8/8] use ApiResonse::json helper --- crates/defguard_core/src/handlers/proxy.rs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 30f8b5f096..ebbfa5efe0 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -4,7 +4,7 @@ use axum::{ }; use defguard_common::{db::models::proxy::Proxy, types::proxy::ProxyControlMessage}; use reqwest::StatusCode; -use serde_json::{Value, json}; +use serde_json::Value; use utoipa::ToSchema; use crate::{ @@ -46,14 +46,8 @@ pub(crate) async fn proxy_details( ); let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; let response = match proxy { - Some(proxy) => ApiResponse { - json: json!(proxy), - status: StatusCode::OK, - }, - None => ApiResponse { - json: Value::Null, - status: StatusCode::NOT_FOUND, - }, + Some(proxy) => ApiResponse::json(proxy, StatusCode::OK), + None => ApiResponse::json(Value::Null, StatusCode::NOT_FOUND), }; info!( "User {} displayed details for proxy {proxy_id}", @@ -92,10 +86,7 @@ pub(crate) async fn update_proxy( let Some(mut proxy) = proxy else { warn!("Proxy {proxy_id} not found"); - return Ok(ApiResponse { - json: Value::Null, - status: StatusCode::NOT_FOUND, - }); + return Ok(ApiResponse::json(Value::Null, StatusCode::NOT_FOUND)); }; let before = proxy.clone(); @@ -143,10 +134,7 @@ pub(crate) async fn delete_proxy( let Some(proxy) = proxy else { warn!("Proxy {proxy_id} not found"); - return Ok(ApiResponse { - json: Value::Null, - status: StatusCode::NOT_FOUND, - }); + return Ok(ApiResponse::json(Value::Null, StatusCode::NOT_FOUND)); }; // Disconnect the proxy