From a4e66ffc17d6ccc7dc3fd776519bafe077c0be5a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 29 Jan 2026 10:04:42 +0100 Subject: [PATCH 01/14] edge edit page --- web/src/pages/EdgeEditPage/EdgeEditPage.tsx | 29 +++++++++++++++++++ web/src/routeTree.gen.ts | 22 ++++++++++++++ .../_default/edge/$edgeId/edit.tsx | 11 +++++++ web/src/shared/api/api.ts | 4 +++ web/src/shared/api/types.ts | 8 +++++ web/src/shared/query.ts | 7 +++++ 6 files changed, 81 insertions(+) create mode 100644 web/src/pages/EdgeEditPage/EdgeEditPage.tsx create mode 100644 web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx diff --git a/web/src/pages/EdgeEditPage/EdgeEditPage.tsx b/web/src/pages/EdgeEditPage/EdgeEditPage.tsx new file mode 100644 index 0000000000..6a47807a58 --- /dev/null +++ b/web/src/pages/EdgeEditPage/EdgeEditPage.tsx @@ -0,0 +1,29 @@ +import { Link } from '@tanstack/react-router'; +import { EditPage } from '../../shared/components/EditPage/EditPage'; + +const breadcrumbsLinks = [ + + Notifications + , + + SMTP Configuration + , +]; + +export const EdgeEditPage = () => { + return ( + + TODO + + ); +}; diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index ead13ae164..f9d5bcf648 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -53,6 +53,7 @@ import { Route as AuthorizedDefaultAclAliasesRouteImport } from './routes/_autho import { Route as AuthorizedDefaultAclAddRuleRouteImport } from './routes/_authorized/_default/acl/add-rule' import { Route as AuthorizedDefaultAclAddAliasRouteImport } from './routes/_authorized/_default/acl/add-alias' import { Route as AuthorizedDefaultLocationsLocationIdEditRouteImport } from './routes/_authorized/_default/locations/$locationId/edit' +import { Route as AuthorizedDefaultEdgeEdgeIdEditRouteImport } from './routes/_authorized/_default/edge/$edgeId/edit' const SnackbarRoute = SnackbarRouteImport.update({ id: '/snackbar', @@ -297,6 +298,12 @@ const AuthorizedDefaultLocationsLocationIdEditRoute = path: '/locations/$locationId/edit', getParentRoute: () => AuthorizedDefaultRoute, } as any) +const AuthorizedDefaultEdgeEdgeIdEditRoute = + AuthorizedDefaultEdgeEdgeIdEditRouteImport.update({ + id: '/edge/$edgeId/edit', + path: '/edge/$edgeId/edit', + getParentRoute: () => AuthorizedDefaultRoute, + } as any) export interface FileRoutesByFullPath { '/404': typeof R404Route @@ -340,6 +347,7 @@ export interface FileRoutesByFullPath { '/locations': typeof AuthorizedDefaultLocationsIndexRoute '/settings': typeof AuthorizedDefaultSettingsIndexRoute '/vpn-overview': typeof AuthorizedDefaultVpnOverviewIndexRoute + '/edge/$edgeId/edit': typeof AuthorizedDefaultEdgeEdgeIdEditRoute '/locations/$locationId/edit': typeof AuthorizedDefaultLocationsLocationIdEditRoute } export interface FileRoutesByTo { @@ -383,6 +391,7 @@ export interface FileRoutesByTo { '/locations': typeof AuthorizedDefaultLocationsIndexRoute '/settings': typeof AuthorizedDefaultSettingsIndexRoute '/vpn-overview': typeof AuthorizedDefaultVpnOverviewIndexRoute + '/edge/$edgeId/edit': typeof AuthorizedDefaultEdgeEdgeIdEditRoute '/locations/$locationId/edit': typeof AuthorizedDefaultLocationsLocationIdEditRoute } export interface FileRoutesById { @@ -430,6 +439,7 @@ export interface FileRoutesById { '/_authorized/_default/locations/': typeof AuthorizedDefaultLocationsIndexRoute '/_authorized/_default/settings/': typeof AuthorizedDefaultSettingsIndexRoute '/_authorized/_default/vpn-overview/': typeof AuthorizedDefaultVpnOverviewIndexRoute + '/_authorized/_default/edge/$edgeId/edit': typeof AuthorizedDefaultEdgeEdgeIdEditRoute '/_authorized/_default/locations/$locationId/edit': typeof AuthorizedDefaultLocationsLocationIdEditRoute } export interface FileRouteTypes { @@ -476,6 +486,7 @@ export interface FileRouteTypes { | '/locations' | '/settings' | '/vpn-overview' + | '/edge/$edgeId/edit' | '/locations/$locationId/edit' fileRoutesByTo: FileRoutesByTo to: @@ -519,6 +530,7 @@ export interface FileRouteTypes { | '/locations' | '/settings' | '/vpn-overview' + | '/edge/$edgeId/edit' | '/locations/$locationId/edit' id: | '__root__' @@ -565,6 +577,7 @@ export interface FileRouteTypes { | '/_authorized/_default/locations/' | '/_authorized/_default/settings/' | '/_authorized/_default/vpn-overview/' + | '/_authorized/_default/edge/$edgeId/edit' | '/_authorized/_default/locations/$locationId/edit' fileRoutesById: FileRoutesById } @@ -887,6 +900,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedDefaultLocationsLocationIdEditRouteImport parentRoute: typeof AuthorizedDefaultRoute } + '/_authorized/_default/edge/$edgeId/edit': { + id: '/_authorized/_default/edge/$edgeId/edit' + path: '/edge/$edgeId/edit' + fullPath: '/edge/$edgeId/edit' + preLoaderRoute: typeof AuthorizedDefaultEdgeEdgeIdEditRouteImport + parentRoute: typeof AuthorizedDefaultRoute + } } } @@ -913,6 +933,7 @@ interface AuthorizedDefaultRouteChildren { AuthorizedDefaultLocationsIndexRoute: typeof AuthorizedDefaultLocationsIndexRoute AuthorizedDefaultSettingsIndexRoute: typeof AuthorizedDefaultSettingsIndexRoute AuthorizedDefaultVpnOverviewIndexRoute: typeof AuthorizedDefaultVpnOverviewIndexRoute + AuthorizedDefaultEdgeEdgeIdEditRoute: typeof AuthorizedDefaultEdgeEdgeIdEditRoute AuthorizedDefaultLocationsLocationIdEditRoute: typeof AuthorizedDefaultLocationsLocationIdEditRoute } @@ -944,6 +965,7 @@ const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultSettingsIndexRoute: AuthorizedDefaultSettingsIndexRoute, AuthorizedDefaultVpnOverviewIndexRoute: AuthorizedDefaultVpnOverviewIndexRoute, + AuthorizedDefaultEdgeEdgeIdEditRoute: AuthorizedDefaultEdgeEdgeIdEditRoute, AuthorizedDefaultLocationsLocationIdEditRoute: AuthorizedDefaultLocationsLocationIdEditRoute, } diff --git a/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx b/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx new file mode 100644 index 0000000000..fdd454bdfa --- /dev/null +++ b/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { EdgeEditPage } from '../../../../../pages/EdgeEditPage/EdgeEditPage'; +import { getEdgeQueryOptions } from '../../../../../shared/query'; + +export const Route = createFileRoute('/_authorized/_default/edge/$edgeId/edit')({ + loader: async ({ context, params }) => { + const parsedId = parseInt(params.edgeId, 10); + return context.queryClient.ensureQueryData(getEdgeQueryOptions(parsedId)); + }, + component: EdgeEditPage, +}); diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 7f402a3bcf..52b28abfda 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -36,6 +36,7 @@ import type { DeleteAuthKeyRequest, DeleteGatewayRequest, Device, + Edge, EditAclAliasRequest, EditAclRuleRequest, EditGroupRequest, @@ -353,6 +354,9 @@ const api = { mail: { sendTestEmail: (data: { email: string }) => client.post('/mail/test', data), }, + edge: { + getEdge: (edgeId: number) => client.get(`/edge/${edgeId}`), + }, acl: { alias: { getAliases: () => client.get('/acl/alias'), diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 942eaea980..218e248b1e 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -908,6 +908,14 @@ export type ActivityLogSortKey = | 'module' | 'device'; +export interface Edge { + id: number; + name: string; + address: string; + port: number; + publicAddress: string; +} + export interface PaginationParams { page?: number; } diff --git a/web/src/shared/query.ts b/web/src/shared/query.ts index 0e6a1f6d1a..c38c622f30 100644 --- a/web/src/shared/query.ts +++ b/web/src/shared/query.ts @@ -28,6 +28,13 @@ export const getLocationsQueryOptions = queryOptions({ select: (resp) => resp.data, }); +export const getEdgeQueryOptions = (id: number) => + queryOptions({ + queryFn: () => api.edge.getEdge(id), + queryKey: ['edge', id], + select: (resp) => resp.data, + }); + export const getNetworkDevicesQueryOptions = queryOptions({ queryFn: api.network_device.getDevices, queryKey: ['device', 'network'], From eabf9222ec9298c9d601b9caa855af34994d9259 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 29 Jan 2026 11:01:31 +0100 Subject: [PATCH 02/14] details api --- crates/defguard_common/src/db/models/proxy.rs | 4 +- crates/defguard_core/src/handlers/mod.rs | 1 + crates/defguard_core/src/handlers/proxy.rs | 45 +++++++++++++++++++ crates/defguard_core/src/lib.rs | 36 +++++---------- web/src/pages/EdgeEditPage/EdgeEditPage.tsx | 11 ++++- web/src/shared/api/api.ts | 2 +- 6 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 crates/defguard_core/src/handlers/proxy.rs diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 0df6d59331..20b72b62f7 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -1,10 +1,12 @@ use chrono::NaiveDateTime; use model_derive::Model; +use serde::Serialize; use sqlx::PgPool; +use utoipa::ToSchema; use crate::db::{Id, NoId}; -#[derive(Model)] +#[derive(Model, Serialize, ToSchema)] pub struct Proxy { pub id: I, pub name: String, diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index f9c1d197f7..5d9949d5a3 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -39,6 +39,7 @@ pub mod network_devices; pub mod openid_clients; pub mod openid_flow; pub(crate) mod pagination; +pub(crate) mod proxy; pub(crate) mod settings; pub(crate) mod ssh_authorized_keys; pub(crate) mod support; diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs new file mode 100644 index 0000000000..8b4cc64ab4 --- /dev/null +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -0,0 +1,45 @@ +use axum::extract::{Path, State}; +use reqwest::StatusCode; +use serde_json::{json, Value}; +use defguard_common::db::models::proxy::Proxy; + +use crate::{appstate::AppState, auth::AdminRole, handlers::{ApiResponse, ApiResult}}; + +#[utoipa::path( + get, + path = "/api/v1/proxy/{proxy_id}", + responses( + (status = 200, description = "Edge details", body = Proxy), + (status = 401, description = "Unauthorized to get edge details.", body = ApiResponse, example = json!({"msg": "Session is required"})), + (status = 403, description = "You don't have permission to get edge details.", body = ApiResponse, example = json!({"msg": "access denied"})), + (status = 404, description = "Edge not found", body = ApiResponse, example = json!({"msg": "network not found"})), + (status = 500, description = "Unable to get edge details.", body = ApiResponse, example = json!({"msg": "Internal server error"})) + ), + security( + ("cookie" = []), + ("api_token" = []) + ) +)] +pub(crate) async fn proxy_details( + Path(proxy_id): Path, + _role: AdminRole, + State(appstate): State, +) -> ApiResult { + debug!("Displaying details for proxy {proxy_id}"); + 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, + }, + }; + debug!("Displayed details for proxy {proxy_id}"); + + Ok(response) +} diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 04611ca25c..717b36afbd 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -109,52 +109,36 @@ use crate::{ }, grpc::{WorkerState, gateway::events::GatewayEvent}, handlers::{ - app_info::get_app_info, - auth::{ + app_info::get_app_info, auth::{ authenticate, email_mfa_code, email_mfa_disable, email_mfa_enable, email_mfa_init, logout, mfa_disable, mfa_enable, recovery_code, request_email_mfa_code, totp_code, totp_disable, totp_enable, totp_secret, webauthn_end, webauthn_finish, webauthn_init, webauthn_start, - }, - ca::create_ca, - component_setup::setup_gateway_tls_stream, - forward_auth::forward_auth, - group::{ + }, ca::create_ca, component_setup::setup_gateway_tls_stream, forward_auth::forward_auth, group::{ add_group_member, create_group, delete_group, get_group, list_groups, modify_group, remove_group_member, - }, - mail::{send_support_data, test_mail}, - openid_clients::{ + }, mail::{send_support_data, test_mail}, openid_clients::{ add_openid_client, change_openid_client, change_openid_client_state, delete_openid_client, get_openid_client, list_openid_clients, - }, - openid_flow::{ + }, openid_flow::{ authorization, discovery_keys, openid_configuration, secure_authorization, token, userinfo, - }, - settings::{ + }, proxy::proxy_details, settings::{ get_settings, get_settings_essentials, patch_settings, set_default_branding, test_ldap_settings, update_settings, - }, - ssh_authorized_keys::get_authorized_keys, - support::{configuration, logs}, - updates::outdated_components, - user::{ + }, ssh_authorized_keys::get_authorized_keys, support::{configuration, logs}, updates::outdated_components, user::{ add_user, change_password, change_self_password, delete_authorized_app, delete_security_key, delete_user, get_user, list_users, me, modify_user, reset_password, start_enrollment, start_remote_desktop_configuration, username_available, - }, - webhooks::{ + }, webhooks::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, - }, - wireguard::{ + }, wireguard::{ add_device, add_user_devices, change_gateway, create_network, create_network_token, delete_device, delete_network, devices_stats, download_config, gateway_status, get_device, import_network, list_devices, list_networks, list_user_devices, modify_device, modify_network, network_details, network_stats, remove_gateway, - }, - worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, + }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker} }, location_management::sync_location_allowed_devices, version::IncompatibleComponents, @@ -359,6 +343,8 @@ pub fn build_webapp( .route("/activity_log", get(get_activity_log_events)) // Certificate authority .route("/ca", post(create_ca)) + // Proxy routes + .route("/proxy/{proxy_id}", get(proxy_details)) // Proxy setup with SSE .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); diff --git a/web/src/pages/EdgeEditPage/EdgeEditPage.tsx b/web/src/pages/EdgeEditPage/EdgeEditPage.tsx index 6a47807a58..7ef4b726d9 100644 --- a/web/src/pages/EdgeEditPage/EdgeEditPage.tsx +++ b/web/src/pages/EdgeEditPage/EdgeEditPage.tsx @@ -1,5 +1,8 @@ import { Link } from '@tanstack/react-router'; +import { useNavigate, useParams } from '@tanstack/react-router'; import { EditPage } from '../../shared/components/EditPage/EditPage'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { getEdgeQueryOptions } from '../../shared/query'; const breadcrumbsLinks = [ { + const { edgeId: paramsId } = useParams({ + from: '/_authorized/_default/edge/$edgeId/edit', + }); + const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(paramsId))); return ( - TODO + ID: {paramsId} name: {edge.name} ); }; diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 52b28abfda..756dee81c1 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -355,7 +355,7 @@ const api = { sendTestEmail: (data: { email: string }) => client.post('/mail/test', data), }, edge: { - getEdge: (edgeId: number) => client.get(`/edge/${edgeId}`), + getEdge: (edgeId: number) => client.get(`/proxy/${edgeId}`), }, acl: { alias: { From c9d39be6b99623df3a207ef3352edb5c1ce62577 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 29 Jan 2026 14:05:16 +0100 Subject: [PATCH 03/14] modify proxy api andpoint & form --- crates/defguard_common/src/db/models/proxy.rs | 18 +- .../src/db/models/activity_log/metadata.rs | 7 + .../src/db/models/activity_log/mod.rs | 2 + crates/defguard_core/src/events.rs | 6 +- crates/defguard_core/src/handlers/proxy.rs | 81 +++++++-- crates/defguard_core/src/lib.rs | 39 ++-- .../defguard_event_logger/src/description.rs | 3 + crates/defguard_event_logger/src/lib.rs | 14 +- crates/defguard_event_logger/src/message.rs | 6 +- .../defguard_event_router/src/handlers/api.rs | 4 + web/src/pages/EdgeEditPage/EdgeEditPage.tsx | 36 ---- web/src/pages/EditEdgePage/EditEdgePage.tsx | 169 ++++++++++++++++++ .../_default/edge/$edgeId/edit.tsx | 4 +- web/src/shared/api/api.ts | 5 +- web/src/shared/api/types.ts | 2 +- 15 files changed, 326 insertions(+), 70 deletions(-) delete mode 100644 web/src/pages/EdgeEditPage/EdgeEditPage.tsx create mode 100644 web/src/pages/EditEdgePage/EditEdgePage.tsx diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 20b72b62f7..1cebb77e32 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -1,12 +1,14 @@ +use std::fmt; + use chrono::NaiveDateTime; use model_derive::Model; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::PgPool; use utoipa::ToSchema; use crate::db::{Id, NoId}; -#[derive(Model, Serialize, ToSchema)] +#[derive(Clone, Debug, Model, Deserialize, Serialize, ToSchema, PartialEq)] pub struct Proxy { pub id: I, pub name: String, @@ -20,6 +22,18 @@ pub struct Proxy { pub certificate_expiry: Option, } +impl fmt::Display for Proxy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl fmt::Display for Proxy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[ID {}] {}", self.id, self.name) + } +} + impl Proxy { pub fn new>(name: S, address: S, port: i32, public_address: S) -> Self { Self { 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 adeb3888e3..406c821b95 100644 --- a/crates/defguard_core/src/db/models/activity_log/metadata.rs +++ b/crates/defguard_core/src/db/models/activity_log/metadata.rs @@ -6,6 +6,7 @@ use defguard_common::db::{ WireguardNetwork, group::Group, oauth2client::OAuth2Client, + proxy::Proxy, settings::{LdapSyncStatus, OpenIdUsernameHandling, SmtpEncryption}, user::User, }, @@ -564,3 +565,9 @@ pub struct UserSnatBindingModifiedMetadata { pub before: UserSnatBinding, pub after: UserSnatBinding, } + +#[derive(Serialize)] +pub struct ProxyModifiedMetadata { + pub before: Proxy, + pub after: Proxy, +} diff --git a/crates/defguard_core/src/db/models/activity_log/mod.rs b/crates/defguard_core/src/db/models/activity_log/mod.rs index cf9314cf42..a2a10d0eba 100644 --- a/crates/defguard_core/src/db/models/activity_log/mod.rs +++ b/crates/defguard_core/src/db/models/activity_log/mod.rs @@ -115,6 +115,8 @@ pub enum EventType { UserSnatBindingAdded, UserSnatBindingRemoved, UserSnatBindingModified, + // Proxy management + ProxyModified, } #[derive(Model, FromRow, Serialize)] diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index 70bae976cf..bcf102322f 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -5,7 +5,7 @@ use defguard_common::db::{ Id, models::{ AuthenticationKey, Device, MFAMethod, Settings, User, WebAuthn, WireguardNetwork, - group::Group, oauth2client::OAuth2Client, + group::Group, oauth2client::OAuth2Client, proxy::Proxy, }, }; use defguard_proto::proxy::MfaMethod; @@ -297,6 +297,10 @@ pub enum ApiEventType { before: UserSnatBinding, after: UserSnatBinding, }, + ProxyModified { + before: Proxy, + after: Proxy, + }, } /// Events from Web API diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 8b4cc64ab4..0df5259a25 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -1,9 +1,17 @@ -use axum::extract::{Path, State}; -use reqwest::StatusCode; -use serde_json::{json, Value}; +use axum::{ + Json, + extract::{Path, State}, +}; use defguard_common::db::models::proxy::Proxy; +use reqwest::StatusCode; +use serde_json::{Value, json}; -use crate::{appstate::AppState, auth::AdminRole, handlers::{ApiResponse, ApiResult}}; +use crate::{ + appstate::AppState, + auth::{AdminRole, SessionInfo}, + events::{ApiEvent, ApiEventType, ApiRequestContext}, + handlers::{ApiResponse, ApiResult}, +}; #[utoipa::path( get, @@ -28,12 +36,10 @@ pub(crate) async fn proxy_details( debug!("Displaying details for proxy {proxy_id}"); let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; let response = match proxy { - Some(proxy) => { - ApiResponse { - json: json!(proxy), - status: StatusCode::OK, - } - } + Some(proxy) => ApiResponse { + json: json!(proxy), + status: StatusCode::OK, + }, None => ApiResponse { json: Value::Null, status: StatusCode::NOT_FOUND, @@ -43,3 +49,58 @@ pub(crate) async fn proxy_details( Ok(response) } + +#[utoipa::path( + put, + path = "/api/v1/proxy/{proxy_id}", + request_body = Proxy, + responses( + (status = 200, description = "Successfully modified edge.", body = Proxy), + (status = 401, description = "Unauthorized to modify edge.", body = ApiResponse, example = json!({"msg": "Session is required"})), + (status = 403, description = "You don't have permission to modify 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 modify edge.", body = ApiResponse, example = json!({"msg": "Internal server error"})) + ), + security( + ("cookie" = []), + ("api_token" = []) + ) +)] +pub(crate) async fn modify_proxy( + _role: AdminRole, + Path(proxy_id): Path, + State(appstate): State, + session: SessionInfo, + context: ApiRequestContext, + Json(data): Json, +) -> ApiResult { + debug!("User {} updating proxy {proxy_id}", session.user.username); + let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; + + let Some(mut proxy) = proxy else { + warn!("Proxy {proxy_id} not found"); + return Ok(ApiResponse { + json: Value::Null, + status: StatusCode::NOT_FOUND, + }); + }; + let before = proxy.clone(); + + proxy.name = data.name; + proxy.save(&appstate.pool).await?; + + info!("User {} updated proxy {proxy_id}", session.user.username); + + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ProxyModified { + before, + after: proxy.clone(), + }), + })?; + + Ok(ApiResponse { + json: json!(proxy), + status: StatusCode::OK, + }) +} diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 717b36afbd..db274476e0 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -109,36 +109,53 @@ use crate::{ }, grpc::{WorkerState, gateway::events::GatewayEvent}, handlers::{ - app_info::get_app_info, auth::{ + app_info::get_app_info, + auth::{ authenticate, email_mfa_code, email_mfa_disable, email_mfa_enable, email_mfa_init, logout, mfa_disable, mfa_enable, recovery_code, request_email_mfa_code, totp_code, totp_disable, totp_enable, totp_secret, webauthn_end, webauthn_finish, webauthn_init, webauthn_start, - }, ca::create_ca, component_setup::setup_gateway_tls_stream, forward_auth::forward_auth, group::{ + }, + ca::create_ca, + component_setup::setup_gateway_tls_stream, + forward_auth::forward_auth, + group::{ add_group_member, create_group, delete_group, get_group, list_groups, modify_group, remove_group_member, - }, mail::{send_support_data, test_mail}, openid_clients::{ + }, + mail::{send_support_data, test_mail}, + openid_clients::{ add_openid_client, change_openid_client, change_openid_client_state, delete_openid_client, get_openid_client, list_openid_clients, - }, openid_flow::{ + }, + openid_flow::{ authorization, discovery_keys, openid_configuration, secure_authorization, token, userinfo, - }, proxy::proxy_details, settings::{ + }, + proxy::{modify_proxy, proxy_details}, + settings::{ get_settings, get_settings_essentials, patch_settings, set_default_branding, test_ldap_settings, update_settings, - }, ssh_authorized_keys::get_authorized_keys, support::{configuration, logs}, updates::outdated_components, user::{ + }, + ssh_authorized_keys::get_authorized_keys, + support::{configuration, logs}, + updates::outdated_components, + user::{ add_user, change_password, change_self_password, delete_authorized_app, delete_security_key, delete_user, get_user, list_users, me, modify_user, reset_password, start_enrollment, start_remote_desktop_configuration, username_available, - }, webhooks::{ + }, + webhooks::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, - }, wireguard::{ + }, + wireguard::{ add_device, add_user_devices, change_gateway, create_network, create_network_token, delete_device, delete_network, devices_stats, download_config, gateway_status, get_device, import_network, list_devices, list_networks, list_user_devices, modify_device, modify_network, network_details, network_stats, remove_gateway, - }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker} + }, + worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, location_management::sync_location_allowed_devices, version::IncompatibleComponents, @@ -343,8 +360,8 @@ pub fn build_webapp( .route("/activity_log", get(get_activity_log_events)) // Certificate authority .route("/ca", post(create_ca)) - // Proxy routes - .route("/proxy/{proxy_id}", get(proxy_details)) + // Proxy routes + .route("/proxy/{proxy_id}", get(proxy_details).put(modify_proxy)) // Proxy setup with SSE .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); diff --git a/crates/defguard_event_logger/src/description.rs b/crates/defguard_event_logger/src/description.rs index 1ca6377198..2cebdcafbd 100644 --- a/crates/defguard_event_logger/src/description.rs +++ b/crates/defguard_event_logger/src/description.rs @@ -257,6 +257,9 @@ pub fn get_defguard_event_description(event: &DefguardEvent) -> Option { "Public IP bound to devices owned by user {user} changed from {} to {}", before.public_ip, after.public_ip )), + DefguardEvent::ProxyModified { before: _, after } => { + Some(format!("Modified proxy {after}")) + } } } diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 29c6123957..500c5c41d7 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -12,11 +12,11 @@ use defguard_core::db::models::activity_log::{ MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata, - SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, UserMfaDisabledMetadata, - UserModifiedMetadata, UserSnatBindingMetadata, UserSnatBindingModifiedMetadata, - VpnClientMetadata, VpnClientMfaFailedMetadata, VpnClientMfaMetadata, VpnLocationMetadata, - VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata, - WebHookStateChangedMetadata, + ProxyModifiedMetadata, SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, + UserMfaDisabledMetadata, UserModifiedMetadata, UserSnatBindingMetadata, + UserSnatBindingModifiedMetadata, VpnClientMetadata, VpnClientMfaFailedMetadata, + VpnClientMfaMetadata, VpnLocationMetadata, VpnLocationModifiedMetadata, WebHookMetadata, + WebHookModifiedMetadata, WebHookStateChangedMetadata, }, }; use description::{ @@ -468,6 +468,10 @@ pub async fn run_event_logger( }) .ok(), ), + DefguardEvent::ProxyModified { before, after } => ( + EventType::ProxyModified, + serde_json::to_value(ProxyModifiedMetadata { before, after }).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 3d736281eb..2421e40cc8 100644 --- a/crates/defguard_event_logger/src/message.rs +++ b/crates/defguard_event_logger/src/message.rs @@ -5,7 +5,7 @@ use defguard_common::db::{ Id, models::{ AuthenticationKey, Device, MFAMethod, Settings, User, WebAuthn, WireguardNetwork, - group::Group, oauth2client::OAuth2Client, + group::Group, oauth2client::OAuth2Client, proxy::Proxy, }, }; use defguard_core::{ @@ -337,6 +337,10 @@ pub enum DefguardEvent { before: UserSnatBinding, after: UserSnatBinding, }, + ProxyModified { + before: Proxy, + after: 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 45e809a48f..d4208b29bd 100644 --- a/crates/defguard_event_router/src/handlers/api.rs +++ b/crates/defguard_event_router/src/handlers/api.rs @@ -389,6 +389,10 @@ impl EventRouter { })), Some(location), ), + ApiEventType::ProxyModified { before, after } => ( + LoggerEvent::Defguard(Box::new(DefguardEvent::ProxyModified { before, after })), + None, + ), }; self.log_event( EventContext::from_api_context(event.context, location), diff --git a/web/src/pages/EdgeEditPage/EdgeEditPage.tsx b/web/src/pages/EdgeEditPage/EdgeEditPage.tsx deleted file mode 100644 index 7ef4b726d9..0000000000 --- a/web/src/pages/EdgeEditPage/EdgeEditPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Link } from '@tanstack/react-router'; -import { useNavigate, useParams } from '@tanstack/react-router'; -import { EditPage } from '../../shared/components/EditPage/EditPage'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { getEdgeQueryOptions } from '../../shared/query'; - -const breadcrumbsLinks = [ - - Notifications - , - - SMTP Configuration - , -]; - -export const EdgeEditPage = () => { - const { edgeId: paramsId } = useParams({ - from: '/_authorized/_default/edge/$edgeId/edit', - }); - const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(paramsId))); - return ( - - ID: {paramsId} name: {edge.name} - - ); -}; diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx new file mode 100644 index 0000000000..f41f20792f --- /dev/null +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -0,0 +1,169 @@ +import { Link, useNavigate } from '@tanstack/react-router'; +import z from 'zod'; +import { m } from '../../paraglide/messages'; +import { useParams } from '@tanstack/react-router'; +import { EditPage } from '../../shared/components/EditPage/EditPage'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; +import { getEdgeQueryOptions } from '../../shared/query'; +import api from '../../shared/api/api'; +import type { Edge } from '../../shared/api/types'; +import { useMemo } from 'react'; +import { useAppForm } from '../../shared/form'; +import { formChangeLogic } from '../../shared/formLogic'; +import { EditPageFormSection } from '../../shared/components/EditPageFormSection/EditPageFormSection'; +import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../shared/defguard-ui/types'; +import { EditPageControls } from '../../shared/components/EditPageControls/EditPageControls'; + +const breadcrumbsLinks = [ + + Notifications + , + + SMTP Configuration + , +]; + +export const EditEdgePage = () => { + const { edgeId: paramsId } = useParams({ + from: '/_authorized/_default/edge/$edgeId/edit', + }); + const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(paramsId))); + return ( + + + + ); +}; + +const formSchema = z.object({ + name: z.string(m.form_error_required()).min(1, m.form_error_required()), + address: z.string(), + port: z.number(), + public_address: z.string(), +}); + +type FormFields = z.infer; + +const EditEdgeForm = ({ edge }: { edge: Edge }) => { + const navigate = useNavigate(); + + const { mutateAsync: editEdge } = useMutation({ + mutationFn: api.edge.editEdge, + meta: { + invalidate: ['edge'], + }, + onSuccess: () => { + navigate({ + // TODO(jck) + to: '/locations', + replace: true, + }); + }, + }); + + const { mutate: deleteEdge, isPending: deletePending } = useMutation({ + mutationFn: () => api.edge.deleteEdge(edge.id), + meta: { + invalidate: ['edge'], + }, + onSuccess: () => { + navigate({ + // TODO(jck) + to: '/locations', + replace: true, + }); + }, + }); + + const defaultValues = useMemo( + (): FormFields => ({ ...edge }), + [edge], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: async ({ value }) => { + await editEdge({ + ...value, + id: edge.id, + }); + }, + }); + + return ( +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + + {(field) => } + + + + {(field) => } + + + + {(field) => } + + + + {(field) => } + + + ({ + isSubmitting: form.isSubmitting, + isDefault: form.isPristine || form.isDefaultValue, + })} + > + {({ isDefault, isSubmitting }) => ( + { + deleteEdge(); + }, + loading: deletePending, + disabled: isSubmitting, + }} + cancelProps={{ + onClick: () => { + window.history.back(); + }, + }} + submitProps={{ + loading: isSubmitting, + disabled: isDefault, + onClick: () => { + form.handleSubmit(); + }, + }} + /> + )} + + +
+ ); +}; diff --git a/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx b/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx index fdd454bdfa..c80468125f 100644 --- a/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx +++ b/web/src/routes/_authorized/_default/edge/$edgeId/edit.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router'; -import { EdgeEditPage } from '../../../../../pages/EdgeEditPage/EdgeEditPage'; +import { EditEdgePage } from '../../../../../pages/EditEdgePage/EditEdgePage'; import { getEdgeQueryOptions } from '../../../../../shared/query'; export const Route = createFileRoute('/_authorized/_default/edge/$edgeId/edit')({ @@ -7,5 +7,5 @@ export const Route = createFileRoute('/_authorized/_default/edge/$edgeId/edit')( const parsedId = parseInt(params.edgeId, 10); return context.queryClient.ensureQueryData(getEdgeQueryOptions(parsedId)); }, - component: EdgeEditPage, + component: EditEdgePage, }); diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 756dee81c1..e94862274d 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -355,7 +355,10 @@ const api = { sendTestEmail: (data: { email: string }) => client.post('/mail/test', data), }, edge: { - getEdge: (edgeId: number) => client.get(`/proxy/${edgeId}`), + getEdge: (edgeId: number | string) => client.get(`/proxy/${edgeId}`), + editEdge: (data: Edge) => + client.put(`/proxy/${data.id}`, data), + deleteEdge: (edgeId: number | string) => client.delete(`/proxy/${edgeId}`), }, acl: { alias: { diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 218e248b1e..639e25281f 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -913,7 +913,7 @@ export interface Edge { name: string; address: string; port: number; - publicAddress: string; + public_address: string; } export interface PaginationParams { From cc44fe249e527c4ae2f2f224d8e4b6f276b0e93f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 10:41:04 +0100 Subject: [PATCH 04/14] struct ProxyUpdateData --- crates/defguard_common/src/db/models/proxy.rs | 4 ++-- crates/defguard_core/src/handlers/proxy.rs | 12 +++++++++--- crates/defguard_core/src/lib.rs | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 1cebb77e32..167965a9cf 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -2,13 +2,13 @@ use std::fmt; use chrono::NaiveDateTime; use model_derive::Model; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use sqlx::PgPool; use utoipa::ToSchema; use crate::db::{Id, NoId}; -#[derive(Clone, Debug, Model, Deserialize, Serialize, ToSchema, PartialEq)] +#[derive(Clone, Debug, Model, Serialize, ToSchema, PartialEq)] pub struct Proxy { pub id: I, pub name: String, diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 0df5259a25..e4f1c08038 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -5,6 +5,7 @@ use axum::{ use defguard_common::db::models::proxy::Proxy; use reqwest::StatusCode; use serde_json::{Value, json}; +use utoipa::ToSchema; use crate::{ appstate::AppState, @@ -13,6 +14,11 @@ use crate::{ handlers::{ApiResponse, ApiResult}, }; +#[derive(Deserialize, ToSchema)] +pub(crate) struct ProxyUpdateData { + name: String, +} + #[utoipa::path( get, path = "/api/v1/proxy/{proxy_id}", @@ -55,7 +61,7 @@ pub(crate) async fn proxy_details( path = "/api/v1/proxy/{proxy_id}", request_body = Proxy, responses( - (status = 200, description = "Successfully modified edge.", body = Proxy), + (status = 200, description = "Successfully modified edge.", body = ProxyUpdateData), (status = 401, description = "Unauthorized to modify edge.", body = ApiResponse, example = json!({"msg": "Session is required"})), (status = 403, description = "You don't have permission to modify an edge.", body = ApiResponse, example = json!({"msg": "access denied"})), (status = 404, description = "Edge not found", body = ApiResponse, example = json!({"msg": "proxy not found"})), @@ -66,13 +72,13 @@ pub(crate) async fn proxy_details( ("api_token" = []) ) )] -pub(crate) async fn modify_proxy( +pub(crate) async fn update_proxy( _role: AdminRole, Path(proxy_id): Path, State(appstate): State, session: SessionInfo, context: ApiRequestContext, - Json(data): Json, + Json(data): Json, ) -> ApiResult { debug!("User {} updating proxy {proxy_id}", session.user.username); let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index db274476e0..b542ead1b0 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::{modify_proxy, proxy_details}, + proxy::{proxy_details, update_proxy}, settings::{ get_settings, get_settings_essentials, patch_settings, set_default_branding, test_ldap_settings, update_settings, @@ -361,7 +361,7 @@ pub fn build_webapp( // Certificate authority .route("/ca", post(create_ca)) // Proxy routes - .route("/proxy/{proxy_id}", get(proxy_details).put(modify_proxy)) + .route("/proxy/{proxy_id}", get(proxy_details).put(update_proxy)) // Proxy setup with SSE .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); From 47cda225aabcc58726fc98356b1dfaa526ed58b4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 11:34:26 +0100 Subject: [PATCH 05/14] edge list page, breadcrumbs --- web/messages/en/edge.json | 4 ++ web/project.inlang/settings.json | 1 + web/src/pages/EdgeListPage/EdgeListPage.tsx | 11 +++++ web/src/pages/EditEdgePage/EditEdgePage.tsx | 41 +++++++------------ web/src/routeTree.gen.ts | 22 ++++++++++ .../_authorized/_default/edge/index.tsx | 6 +++ web/src/shared/api/api.ts | 3 +- 7 files changed, 60 insertions(+), 28 deletions(-) create mode 100644 web/messages/en/edge.json create mode 100644 web/src/pages/EdgeListPage/EdgeListPage.tsx create mode 100644 web/src/routes/_authorized/_default/edge/index.tsx diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json new file mode 100644 index 0000000000..e8e6ec63f9 --- /dev/null +++ b/web/messages/en/edge.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "edge_list_title": "Edge components" +} diff --git a/web/project.inlang/settings.json b/web/project.inlang/settings.json index a080a2894b..17f836ebbb 100644 --- a/web/project.inlang/settings.json +++ b/web/project.inlang/settings.json @@ -18,6 +18,7 @@ "./messages/{locale}/groups.json", "./messages/{locale}/openid.json", "./messages/{locale}/activity.json", + "./messages/{locale}/edge.json", "./messages/{locale}/edge_wizard.json", "./messages/{locale}/settings.json", "./messages/{locale}/gateway_wizard.json" diff --git a/web/src/pages/EdgeListPage/EdgeListPage.tsx b/web/src/pages/EdgeListPage/EdgeListPage.tsx new file mode 100644 index 0000000000..fbd392c76a --- /dev/null +++ b/web/src/pages/EdgeListPage/EdgeListPage.tsx @@ -0,0 +1,11 @@ +import { m } from '../../paraglide/messages'; +import { Page } from '../../shared/components/Page/Page'; +import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout'; + +export const EdgeListPage = () => { + return ( + + TODO + + ); +}; diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index f41f20792f..9f79c06859 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -1,32 +1,25 @@ -import { Link, useNavigate } from '@tanstack/react-router'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; +import { Link, useNavigate, useParams } from '@tanstack/react-router'; +import { useMemo } from 'react'; import z from 'zod'; import { m } from '../../paraglide/messages'; -import { useParams } from '@tanstack/react-router'; -import { EditPage } from '../../shared/components/EditPage/EditPage'; -import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; -import { getEdgeQueryOptions } from '../../shared/query'; import api from '../../shared/api/api'; import type { Edge } from '../../shared/api/types'; -import { useMemo } from 'react'; -import { useAppForm } from '../../shared/form'; -import { formChangeLogic } from '../../shared/formLogic'; +import { EditPage } from '../../shared/components/EditPage/EditPage'; +import { EditPageControls } from '../../shared/components/EditPageControls/EditPageControls'; import { EditPageFormSection } from '../../shared/components/EditPageFormSection/EditPageFormSection'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../shared/defguard-ui/types'; -import { EditPageControls } from '../../shared/components/EditPageControls/EditPageControls'; +import { useAppForm } from '../../shared/form'; +import { formChangeLogic } from '../../shared/formLogic'; +import { getEdgeQueryOptions } from '../../shared/query'; const breadcrumbsLinks = [ - - Notifications + + Edge components , - - SMTP Configuration + + Edit , ]; @@ -65,8 +58,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { }, onSuccess: () => { navigate({ - // TODO(jck) - to: '/locations', + to: '/edge', replace: true, }); }, @@ -86,10 +78,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { }, }); - const defaultValues = useMemo( - (): FormFields => ({ ...edge }), - [edge], - ); + const defaultValues = useMemo((): FormFields => ({ ...edge }), [edge]); const form = useAppForm({ defaultValues, @@ -150,7 +139,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { }} cancelProps={{ onClick: () => { - window.history.back(); + navigate({to: "/edge"}); }, }} submitProps={{ diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index f9d5bcf648..ed5766da8b 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -39,6 +39,7 @@ import { Route as AuthorizedDefaultActivityRouteImport } from './routes/_authori import { Route as AuthorizedDefaultVpnOverviewIndexRouteImport } from './routes/_authorized/_default/vpn-overview/index' import { Route as AuthorizedDefaultSettingsIndexRouteImport } from './routes/_authorized/_default/settings/index' import { Route as AuthorizedDefaultLocationsIndexRouteImport } from './routes/_authorized/_default/locations/index' +import { Route as AuthorizedDefaultEdgeIndexRouteImport } from './routes/_authorized/_default/edge/index' import { Route as AuthorizedDefaultVpnOverviewLocationIdRouteImport } from './routes/_authorized/_default/vpn-overview/$locationId' import { Route as AuthorizedDefaultUserUsernameRouteImport } from './routes/_authorized/_default/user/$username' import { Route as AuthorizedDefaultSettingsSmtpRouteImport } from './routes/_authorized/_default/settings/smtp' @@ -214,6 +215,12 @@ const AuthorizedDefaultLocationsIndexRoute = path: '/locations/', getParentRoute: () => AuthorizedDefaultRoute, } as any) +const AuthorizedDefaultEdgeIndexRoute = + AuthorizedDefaultEdgeIndexRouteImport.update({ + id: '/edge/', + path: '/edge/', + getParentRoute: () => AuthorizedDefaultRoute, + } as any) const AuthorizedDefaultVpnOverviewLocationIdRoute = AuthorizedDefaultVpnOverviewLocationIdRouteImport.update({ id: '/vpn-overview/$locationId', @@ -344,6 +351,7 @@ 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 @@ -388,6 +396,7 @@ export interface FileRoutesByTo { '/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 @@ -436,6 +445,7 @@ export interface FileRoutesById { '/_authorized/_default/settings/smtp': typeof AuthorizedDefaultSettingsSmtpRoute '/_authorized/_default/user/$username': typeof AuthorizedDefaultUserUsernameRoute '/_authorized/_default/vpn-overview/$locationId': typeof AuthorizedDefaultVpnOverviewLocationIdRoute + '/_authorized/_default/edge/': typeof AuthorizedDefaultEdgeIndexRoute '/_authorized/_default/locations/': typeof AuthorizedDefaultLocationsIndexRoute '/_authorized/_default/settings/': typeof AuthorizedDefaultSettingsIndexRoute '/_authorized/_default/vpn-overview/': typeof AuthorizedDefaultVpnOverviewIndexRoute @@ -483,6 +493,7 @@ export interface FileRouteTypes { | '/settings/smtp' | '/user/$username' | '/vpn-overview/$locationId' + | '/edge' | '/locations' | '/settings' | '/vpn-overview' @@ -527,6 +538,7 @@ export interface FileRouteTypes { | '/settings/smtp' | '/user/$username' | '/vpn-overview/$locationId' + | '/edge' | '/locations' | '/settings' | '/vpn-overview' @@ -574,6 +586,7 @@ export interface FileRouteTypes { | '/_authorized/_default/settings/smtp' | '/_authorized/_default/user/$username' | '/_authorized/_default/vpn-overview/$locationId' + | '/_authorized/_default/edge/' | '/_authorized/_default/locations/' | '/_authorized/_default/settings/' | '/_authorized/_default/vpn-overview/' @@ -802,6 +815,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedDefaultLocationsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } + '/_authorized/_default/edge/': { + id: '/_authorized/_default/edge/' + path: '/edge' + fullPath: '/edge' + preLoaderRoute: typeof AuthorizedDefaultEdgeIndexRouteImport + parentRoute: typeof AuthorizedDefaultRoute + } '/_authorized/_default/vpn-overview/$locationId': { id: '/_authorized/_default/vpn-overview/$locationId' path: '/vpn-overview/$locationId' @@ -930,6 +950,7 @@ interface AuthorizedDefaultRouteChildren { AuthorizedDefaultSettingsSmtpRoute: typeof AuthorizedDefaultSettingsSmtpRoute AuthorizedDefaultUserUsernameRoute: typeof AuthorizedDefaultUserUsernameRoute AuthorizedDefaultVpnOverviewLocationIdRoute: typeof AuthorizedDefaultVpnOverviewLocationIdRoute + AuthorizedDefaultEdgeIndexRoute: typeof AuthorizedDefaultEdgeIndexRoute AuthorizedDefaultLocationsIndexRoute: typeof AuthorizedDefaultLocationsIndexRoute AuthorizedDefaultSettingsIndexRoute: typeof AuthorizedDefaultSettingsIndexRoute AuthorizedDefaultVpnOverviewIndexRoute: typeof AuthorizedDefaultVpnOverviewIndexRoute @@ -961,6 +982,7 @@ const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultUserUsernameRoute: AuthorizedDefaultUserUsernameRoute, AuthorizedDefaultVpnOverviewLocationIdRoute: AuthorizedDefaultVpnOverviewLocationIdRoute, + AuthorizedDefaultEdgeIndexRoute: AuthorizedDefaultEdgeIndexRoute, AuthorizedDefaultLocationsIndexRoute: AuthorizedDefaultLocationsIndexRoute, AuthorizedDefaultSettingsIndexRoute: AuthorizedDefaultSettingsIndexRoute, AuthorizedDefaultVpnOverviewIndexRoute: diff --git a/web/src/routes/_authorized/_default/edge/index.tsx b/web/src/routes/_authorized/_default/edge/index.tsx new file mode 100644 index 0000000000..12516233e9 --- /dev/null +++ b/web/src/routes/_authorized/_default/edge/index.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { EdgeListPage } from '../../../../pages/EdgeListPage/EdgeListPage'; + +export const Route = createFileRoute('/_authorized/_default/edge/')({ + component: EdgeListPage, +}); diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index e94862274d..096224f3e2 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -356,8 +356,7 @@ const api = { }, edge: { getEdge: (edgeId: number | string) => client.get(`/proxy/${edgeId}`), - editEdge: (data: Edge) => - client.put(`/proxy/${data.id}`, data), + editEdge: (data: Edge) => client.put(`/proxy/${data.id}`, data), deleteEdge: (edgeId: number | string) => client.delete(`/proxy/${edgeId}`), }, acl: { From 33410b042a87e088eb729b4fd625e641bff94cf0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 11:36:29 +0100 Subject: [PATCH 06/14] cancel proxy edit does history.back --- web/src/pages/EditEdgePage/EditEdgePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 9f79c06859..625fae329f 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -139,7 +139,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { }} cancelProps={{ onClick: () => { - navigate({to: "/edge"}); + window.history.back(); }, }} submitProps={{ From 2819b0a59401f8bcfd2050bbb1b8bf7d08893d8d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 12:11:14 +0100 Subject: [PATCH 07/14] translations --- web/messages/en/edge.json | 9 +++++++- web/src/pages/EdgeListPage/EdgeListPage.tsx | 2 +- web/src/pages/EditEdgePage/EditEdgePage.tsx | 23 ++++++++++++--------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json index e8e6ec63f9..ec36a29437 100644 --- a/web/messages/en/edge.json +++ b/web/messages/en/edge.json @@ -1,4 +1,11 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "edge_list_title": "Edge components" + "edge_title": "Edge components", + "edge_edit_title": "Edit edge component", + "edge_edit_general_info": "General information", + "edge_edit_name": "Name", + "edge_edit_address": "IP or Domain", + "edge_edit_port": "gRPC port", + "edge_edit_public_address": "Public domain", + "edge_edit_delete": "Delete" } diff --git a/web/src/pages/EdgeListPage/EdgeListPage.tsx b/web/src/pages/EdgeListPage/EdgeListPage.tsx index fbd392c76a..b1302a6e51 100644 --- a/web/src/pages/EdgeListPage/EdgeListPage.tsx +++ b/web/src/pages/EdgeListPage/EdgeListPage.tsx @@ -4,7 +4,7 @@ import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLa export const EdgeListPage = () => { return ( - + TODO ); diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 625fae329f..ca0f03d6e5 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -30,9 +30,9 @@ export const EditEdgePage = () => { const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(paramsId))); return ( @@ -71,8 +71,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { }, onSuccess: () => { navigate({ - // TODO(jck) - to: '/locations', + to: '/edge', replace: true, }); }, @@ -104,21 +103,25 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { }} > - + - {(field) => } + {(field) => } - {(field) => } + {(field) => ( + + )} - {(field) => } + {(field) => } - {(field) => } + {(field) => ( + + )} { {({ isDefault, isSubmitting }) => ( { deleteEdge(); }, From 03311dd03d4db6da0274d1612727316897538f2c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 12:16:51 +0100 Subject: [PATCH 08/14] fix breadcrumbs --- web/src/pages/EditEdgePage/EditEdgePage.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index ca0f03d6e5..83d7cf0ff1 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -14,20 +14,19 @@ import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { getEdgeQueryOptions } from '../../shared/query'; -const breadcrumbsLinks = [ - - Edge components - , - - Edit - , -]; - export const EditEdgePage = () => { - const { edgeId: paramsId } = useParams({ + const { edgeId } = useParams({ from: '/_authorized/_default/edge/$edgeId/edit', }); - const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(paramsId))); + const breadcrumbsLinks = [ + + Edge components + , + + Edit + , + ]; + const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(edgeId))); return ( Date: Fri, 30 Jan 2026 12:29:40 +0100 Subject: [PATCH 09/14] breadcrumbs show edge name --- web/src/pages/EditEdgePage/EditEdgePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 83d7cf0ff1..1ea3ae0590 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -18,15 +18,15 @@ export const EditEdgePage = () => { const { edgeId } = useParams({ from: '/_authorized/_default/edge/$edgeId/edit', }); + const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(edgeId))); const breadcrumbsLinks = [ Edge components , - Edit + {edge.name} , ]; - const { data: edge } = useSuspenseQuery(getEdgeQueryOptions(Number(edgeId))); return ( Date: Fri, 30 Jan 2026 12:32:38 +0100 Subject: [PATCH 10/14] log user displaying proxy details --- crates/defguard_core/src/handlers/proxy.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index e4f1c08038..b9dd957ebd 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -37,9 +37,13 @@ pub(crate) struct ProxyUpdateData { pub(crate) async fn proxy_details( Path(proxy_id): Path, _role: AdminRole, + session: SessionInfo, State(appstate): State, ) -> ApiResult { - debug!("Displaying details for proxy {proxy_id}"); + debug!( + "User {} displaying details for proxy {proxy_id}", + session.user.username + ); let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; let response = match proxy { Some(proxy) => ApiResponse { @@ -51,7 +55,10 @@ pub(crate) async fn proxy_details( status: StatusCode::NOT_FOUND, }, }; - debug!("Displayed details for proxy {proxy_id}"); + info!( + "User {} displayed details for proxy {proxy_id}", + session.user.username + ); Ok(response) } From e7b3ce1df6e33c2ae57cf9a66fd827a82a12bafe Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 13:31:45 +0100 Subject: [PATCH 11/14] proxy api tests --- crates/defguard_common/src/db/models/proxy.rs | 4 +- crates/defguard_core/src/handlers/mod.rs | 2 +- crates/defguard_core/src/handlers/proxy.rs | 6 +- .../tests/integration/api/mod.rs | 1 + .../tests/integration/api/proxy.rs | 55 +++++++++++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 crates/defguard_core/tests/integration/api/proxy.rs diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 167965a9cf..5b20797702 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -2,13 +2,13 @@ use std::fmt; use chrono::NaiveDateTime; use model_derive::Model; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::PgPool; use utoipa::ToSchema; use crate::db::{Id, NoId}; -#[derive(Clone, Debug, Model, Serialize, ToSchema, PartialEq)] +#[derive(Clone, Debug, Deserialize, Model, Serialize, ToSchema, PartialEq)] pub struct Proxy { pub id: I, pub name: String, diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index 5d9949d5a3..13a2fffdac 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -39,7 +39,7 @@ pub mod network_devices; pub mod openid_clients; pub mod openid_flow; pub(crate) mod pagination; -pub(crate) mod proxy; +pub mod proxy; pub(crate) mod settings; pub(crate) mod ssh_authorized_keys; pub(crate) mod support; diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index b9dd957ebd..f2b21e8440 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -14,9 +14,9 @@ use crate::{ handlers::{ApiResponse, ApiResult}, }; -#[derive(Deserialize, ToSchema)] -pub(crate) struct ProxyUpdateData { - name: String, +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ProxyUpdateData { + pub name: String, } #[utoipa::path( diff --git a/crates/defguard_core/tests/integration/api/mod.rs b/crates/defguard_core/tests/integration/api/mod.rs index 8783bf4a87..4ca59b3356 100644 --- a/crates/defguard_core/tests/integration/api/mod.rs +++ b/crates/defguard_core/tests/integration/api/mod.rs @@ -9,6 +9,7 @@ mod group; mod oauth; mod openid; mod openid_login; +mod proxy; mod settings; mod snat; mod user; diff --git a/crates/defguard_core/tests/integration/api/proxy.rs b/crates/defguard_core/tests/integration/api/proxy.rs new file mode 100644 index 0000000000..ba4ba23c85 --- /dev/null +++ b/crates/defguard_core/tests/integration/api/proxy.rs @@ -0,0 +1,55 @@ +use defguard_common::db::{Id, models::proxy::Proxy}; +use defguard_core::handlers::{Auth, proxy::ProxyUpdateData}; +use reqwest::StatusCode; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use super::common::{make_test_client, setup_pool}; + +#[sqlx::test] +async fn test_update_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 mut proxy = Proxy::new("test", "localhost", 50051, "public.net") + .save(&pool) + .await + .unwrap(); + + // Modify name + let data = ProxyUpdateData { + name: "modified".to_string(), + }; + let response = client + .put(format!("/api/v1/proxy/{}", proxy.id)) + .json(&data) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Verify proxy is modified correctly + let proxy_updated: Proxy = response.json().await; + assert_eq!(proxy_updated.name, "modified"); + proxy.name = "modified".to_string(); + assert_eq!(proxy, proxy_updated); + + // Try to modify other fields + let proxy_before_mods = proxy.clone(); + proxy.address = "otherhost".to_string(); + proxy.port = 50052; + proxy.public_address = "otherpublichost.net".to_string(); + let response = client + .put(format!("/api/v1/proxy/{}", proxy.id)) + .json(&proxy) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + let proxy_updated: Proxy = response.json().await; + assert_eq!(proxy_before_mods, proxy_updated); +} From 868aec4cf42734169547975cac447bb320180b8c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 14:13:12 +0100 Subject: [PATCH 12/14] show update success message using a snackbar --- web/messages/en/edge.json | 3 ++- web/src/pages/EditEdgePage/EditEdgePage.tsx | 12 +++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json index ec36a29437..c2ad34c6eb 100644 --- a/web/messages/en/edge.json +++ b/web/messages/en/edge.json @@ -7,5 +7,6 @@ "edge_edit_address": "IP or Domain", "edge_edit_port": "gRPC port", "edge_edit_public_address": "Public domain", - "edge_edit_delete": "Delete" + "edge_edit_delete": "Delete", + "edge_edit_success": "Edge component updated" } diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 1ea3ae0590..011ea46779 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -13,6 +13,7 @@ import { ThemeSpacing } from '../../shared/defguard-ui/types'; import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { getEdgeQueryOptions } from '../../shared/query'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; export const EditEdgePage = () => { const { edgeId } = useParams({ @@ -56,10 +57,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { invalidate: ['edge'], }, onSuccess: () => { - navigate({ - to: '/edge', - replace: true, - }); + Snackbar.success(m.edge_edit_success()); }, }); @@ -109,17 +107,17 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { {(field) => ( - + )} - {(field) => } + {(field) => } {(field) => ( - + )} From 3c79e7e859af2d7d2aa682b61c2b9dbabb0b0274 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Jan 2026 15:11:16 +0100 Subject: [PATCH 13/14] fix linting --- web/src/pages/EditEdgePage/EditEdgePage.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 011ea46779..4d91f0dbc7 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -9,11 +9,11 @@ import { EditPage } from '../../shared/components/EditPage/EditPage'; import { EditPageControls } from '../../shared/components/EditPageControls/EditPageControls'; import { EditPageFormSection } from '../../shared/components/EditPageFormSection/EditPageFormSection'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../shared/defguard-ui/types'; import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { getEdgeQueryOptions } from '../../shared/query'; -import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; export const EditEdgePage = () => { const { edgeId } = useParams({ @@ -106,9 +106,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { - {(field) => ( - - )} + {(field) => } @@ -116,9 +114,7 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { - {(field) => ( - - )} + {(field) => } Date: Fri, 30 Jan 2026 15:51:18 +0100 Subject: [PATCH 14/14] review fixes --- crates/defguard_core/src/handlers/proxy.rs | 5 +---- web/messages/en/edge.json | 3 ++- web/src/pages/EditEdgePage/EditEdgePage.tsx | 9 ++++++--- web/src/shared/api/types.ts | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index f2b21e8440..6ae43d593d 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -112,8 +112,5 @@ pub(crate) async fn update_proxy( }), })?; - Ok(ApiResponse { - json: json!(proxy), - status: StatusCode::OK, - }) + Ok(ApiResponse::json(proxy, StatusCode::OK)) } diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json index c2ad34c6eb..b58877883f 100644 --- a/web/messages/en/edge.json +++ b/web/messages/en/edge.json @@ -8,5 +8,6 @@ "edge_edit_port": "gRPC port", "edge_edit_public_address": "Public domain", "edge_edit_delete": "Delete", - "edge_edit_success": "Edge component updated" + "edge_edit_success": "Edge component updated", + "edge_edit_failed": "Failed to update edge component" } diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 4d91f0dbc7..7c39c4659f 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -41,9 +41,9 @@ export const EditEdgePage = () => { const formSchema = z.object({ name: z.string(m.form_error_required()).min(1, m.form_error_required()), - address: z.string(), - port: z.number(), - public_address: z.string(), + address: z.string().nullable(), + port: z.number().nullable(), + public_address: z.string().nullable(), }); type FormFields = z.infer; @@ -59,6 +59,9 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { onSuccess: () => { Snackbar.success(m.edge_edit_success()); }, + onError: () => { + Snackbar.error(m.edge_edit_failed()); + }, }); const { mutate: deleteEdge, isPending: deletePending } = useMutation({ diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 194d806bd0..b7754145f9 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -911,9 +911,9 @@ export type ActivityLogSortKey = export interface Edge { id: number; name: string; - address: string; - port: number; - public_address: string; + address: string | null; + port: number | null; + public_address: string | null; } export interface PaginationParams {