From 126c20e5801a508e0754ca91ba8742feced3408a Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:49:52 +0100 Subject: [PATCH 1/9] block adding network device when there is no space in network --- .../defguard_common/src/db/models/device.rs | 25 +++++++++++++------ crates/defguard_core/src/error.rs | 1 + web/messages/en/form.json | 1 + .../AddDeviceModalManualSetupStep.tsx | 7 ++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 65087a3408..bb09c6f08e 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -27,7 +27,9 @@ use crate::{ ModelError, WireguardNetwork, user::User, vpn_client_session::VpnClientSessionState, - wireguard::{LocationMfaMode, NetworkAddressError, ServiceLocationMode}, + wireguard::{ + LocationMfaMode, NetworkAddressError, ServiceLocationMode, WireguardNetworkError, + }, }, }, }; @@ -542,6 +544,8 @@ pub enum DeviceError { NetworkIpAssignmentError(#[from] NetworkAddressError), #[error("Unexpected error: {0}")] Unexpected(String), + #[error("Network {0} is full, no IP addresses available for device")] + NetworkFull(String), } impl Device { @@ -780,13 +784,18 @@ impl Device { continue; } - // FIXME: don't ignore the error. - let Ok(wireguard_network_device) = - network.add_device_to_network(&mut *conn, self, None).await - else { - warn!("Failed to add device {self} to network {network}"); - continue; - }; + let wireguard_network_device = + match network.add_device_to_network(&mut *conn, self, None).await { + Ok(device) => device, + Err(WireguardNetworkError::DeviceNotAllowed(_)) => { + debug!("Device {self} is not allowed in network {network}, skipping"); + continue; + } + Err(err) => { + warn!("Failed to add device {self} to network {network}: {err}"); + return Err(DeviceError::NetworkFull(network.name.clone())); + } + }; debug!( "Assigned IPs {} for device {} (user {}) in network {network}", diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index 5e5edbdd0c..5843fbfd1e 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -122,6 +122,7 @@ impl From for WebError { DeviceError::DatabaseError(_) => Self::DbError(error.to_string()), DeviceError::NetworkIpAssignmentError(_) => Self::ModelError(error.to_string()), DeviceError::Unexpected(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR), + DeviceError::NetworkFull(_) => Self::BadRequest(error.to_string()), } } } diff --git a/web/messages/en/form.json b/web/messages/en/form.json index 0775653714..eb4224e11b 100644 --- a/web/messages/en/form.json +++ b/web/messages/en/form.json @@ -24,6 +24,7 @@ "form_error_current_password": "Current password is incorrect.", "form_error_invalid_key": "Invalid key format", "form_error_key_exists": "Key already exists", + "form_error_network_full": "Network is full, no IP addresses available", "form_error_ip_invalid": "IP is invalid", "form_error_ip_reserved": "IP is not available", "password_form_check_title": "Your password must include:", diff --git a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx index 36203d2816..515153d67e 100644 --- a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx +++ b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx @@ -12,6 +12,7 @@ import { useMemo } from 'react'; import api from '../../../../../api/api'; import type { ApiError } from '../../../../../api/types'; import { SizedBox } from '../../../../../defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../../../../defguard-ui/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../../../../defguard-ui/types'; import { patternValidWireguardKey } from '../../../../../patterns'; import { generateWGKeys } from '../../../../../utils/generateWGKeys'; @@ -107,6 +108,11 @@ export const AddDeviceModalManualSetupStep = () => { }, }, }); + } else if ( + msg?.toLowerCase().includes('network') && + msg?.toLowerCase().includes('full') + ) { + Snackbar.error(m.form_error_network_full()); } }); @@ -114,6 +120,7 @@ export const AddDeviceModalManualSetupStep = () => { if (!createResponse.data.configs.length) { useAddUserDeviceModal.getState().close(); + return; } useAddUserDeviceModal.setState({ From f038e1d451c6aedd0ec30a0f1322294fb3cb1d58 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:26:39 +0100 Subject: [PATCH 2/9] handle error better --- .../defguard_common/src/db/models/device.rs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index bb09c6f08e..542e48587a 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -784,18 +784,24 @@ impl Device { continue; } - let wireguard_network_device = - match network.add_device_to_network(&mut *conn, self, None).await { - Ok(device) => device, - Err(WireguardNetworkError::DeviceNotAllowed(_)) => { - debug!("Device {self} is not allowed in network {network}, skipping"); - continue; - } - Err(err) => { - warn!("Failed to add device {self} to network {network}: {err}"); - return Err(DeviceError::NetworkFull(network.name.clone())); - } - }; + let wireguard_network_device = match network + .add_device_to_network(&mut *conn, self, None) + .await + { + Ok(device) => device, + Err(WireguardNetworkError::DeviceNotAllowed(_)) => { + debug!("Device {self} is not allowed in network {network}, skipping"); + continue; + } + Err(WireguardNetworkError::ModelError(ModelError::CannotCreate)) => { + warn!("Network {network} is full, no IP addresses available for device {self}"); + return Err(DeviceError::NetworkFull(network.name.clone())); + } + Err(err) => { + warn!("Failed to add device {self} to network {network}: {err}"); + return Err(DeviceError::Unexpected(err.to_string())); + } + }; debug!( "Assigned IPs {} for device {} (user {}) in network {network}", From 22b6f80e30a7efc0c71f552763deef8a493de124 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:53:20 +0100 Subject: [PATCH 3/9] change modelerror to deviceerror --- crates/defguard_common/src/db/models/device.rs | 8 +++++--- crates/defguard_common/src/db/models/wireguard.rs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 542e48587a..dd0988bc3a 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -541,6 +541,8 @@ pub enum DeviceError { #[error("Database error")] DatabaseError(#[from] sqlx::Error), #[error(transparent)] + ModelError(#[from] ModelError), + #[error(transparent)] NetworkIpAssignmentError(#[from] NetworkAddressError), #[error("Unexpected error: {0}")] Unexpected(String), @@ -861,7 +863,7 @@ impl Device { /// # Returns /// /// - `Ok(WireguardNetworkDevice)`: A new relation linking this device to its assigned IPs across all subnets. - /// - `Err(ModelError::CannotCreate)`: If any subnet lacks an available IP. + /// - `Err(DeviceError::NetworkFull)`: If any subnet lacks an available IP. pub async fn assign_next_network_ip( &self, transaction: &mut PgConnection, @@ -869,7 +871,7 @@ impl Device { used_ips: &HashSet, reserved_ips: Option<&[IpAddr]>, current_ips: Option<&[IpAddr]>, - ) -> Result { + ) -> Result { debug!( "Assiging IP addresses for device: {} in network {}", self.name, network.name @@ -914,7 +916,7 @@ impl Device { "Failed to assign address for device {} in network {address:?}", self.name, ); - ModelError::CannotCreate + DeviceError::NetworkFull(address.to_string()) })?; // Otherwise, store the IP address diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 23c20d659c..95134cdd6a 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -377,7 +377,7 @@ impl WireguardNetwork { &self, transaction: &mut PgConnection, user_id: Id, - ) -> Result>, ModelError> { + ) -> Result>, DeviceError> { debug!("Fetching all allowed devices for network {self}, user ID {user_id}"); let devices = match self.get_allowed_groups(&mut *transaction).await? { @@ -426,7 +426,7 @@ impl WireguardNetwork { pub async fn add_all_allowed_devices( &self, transaction: &mut PgConnection, - ) -> Result<(), ModelError> { + ) -> Result<(), DeviceError> { info!( "Assigning IPs in network {} for all existing devices ", self From 27a803fa04809d4fd1fe940e8676b1a89143b7b9 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:56:53 +0100 Subject: [PATCH 4/9] change match --- crates/defguard_common/src/db/models/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index dd0988bc3a..ef2682aa58 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -795,7 +795,7 @@ impl Device { debug!("Device {self} is not allowed in network {network}, skipping"); continue; } - Err(WireguardNetworkError::ModelError(ModelError::CannotCreate)) => { + Err(WireguardNetworkError::DeviceError(DeviceError::NetworkFull(_))) => { warn!("Network {network} is full, no IP addresses available for device {self}"); return Err(DeviceError::NetworkFull(network.name.clone())); } From 9fe7db75b3aa8c41f8b2adee1c8d8cc4e50bf49b Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:03:28 +0100 Subject: [PATCH 5/9] add new error --- crates/defguard_core/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index 5843fbfd1e..eb214840b1 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -123,6 +123,7 @@ impl From for WebError { DeviceError::NetworkIpAssignmentError(_) => Self::ModelError(error.to_string()), DeviceError::Unexpected(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR), DeviceError::NetworkFull(_) => Self::BadRequest(error.to_string()), + DeviceError::ModelError(_) => Self::ModelError(error.to_string()), } } } From 8cea7fdae7718a7ca65bcddab49a46cf47ed524a Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:22:35 +0100 Subject: [PATCH 6/9] information when trying to add network device to full network --- .../src/handlers/network_devices.rs | 18 ++++++++++-------- web/messages/en/location.json | 2 ++ .../NetworkDevicesPage/NetworkDevicesTable.tsx | 11 +++++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index 0c03d194b8..fdd6507845 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -349,19 +349,21 @@ pub(crate) async fn find_available_ips( } transaction.commit().await?; - if split_ips.len() == network.address.len() { - debug!( - "Found addresses {:?} for new device i network {} ({:?})", - split_ips, network.name, network.address - ); - Ok(ApiResponse::json(split_ips, StatusCode::OK)) - } else { + if split_ips.len() != network.address.len() { warn!( "Failed to find available IPs for new device in network {} ({:?})", network.name, network.address ); - Ok(ApiResponse::with_status(StatusCode::NOT_FOUND)) + return Err(WebError::BadRequest(format!( + "Network {} is full, no IP addresses available", + network.name + ))); } + debug!( + "Found addresses {:?} for new device in network {} ({:?})", + split_ips, network.name, network.address + ); + Ok(ApiResponse::json(split_ips, StatusCode::OK)) } #[derive(Serialize, Deserialize, Debug)] diff --git a/web/messages/en/location.json b/web/messages/en/location.json index c80b5f1a3b..feecb083cd 100644 --- a/web/messages/en/location.json +++ b/web/messages/en/location.json @@ -9,6 +9,8 @@ "location_delete_failed": "Failed to delete location", "network_device_delete_success": "Network device deleted", "network_device_delete_failed": "Failed to delete network device", + "network_device_add_error": "Failed to load network data for adding device", + "network_device_add_network_full": "Network is full, no IP addresses available for a new device", "location_edit_failed": "Failed to update location", "location_edit_addresses_rewrite_warning": "Changing the Gateway VPN IP address or netmask will automatically reassign any device addresses that fall outside the new network range to a randomly selected available IP address." } diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index 6f8ef16d42..fb4efc2910 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -5,6 +5,7 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table'; +import type { AxiosError } from 'axios'; import { orderBy } from 'lodash-es'; import { useMemo } from 'react'; import { m } from '../../paraglide/messages'; @@ -24,6 +25,7 @@ import { TableBody } from '../../shared/defguard-ui/components/table/TableBody/T import { TableCell } from '../../shared/defguard-ui/components/table/TableCell/TableCell'; import { TableEditCell } from '../../shared/defguard-ui/components/table/TableEditCell/TableEditCell'; import { TableTop } from '../../shared/defguard-ui/components/table/TableTop/TableTop'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { ThemeSize } from '../../shared/defguard-ui/types'; import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; @@ -63,6 +65,15 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { reservedNames, }); }, + onError: (e) => { + console.error(e); + const status = (e as AxiosError).response?.status; + if (status === 400) { + Snackbar.error(m.network_device_add_network_full()); + } else { + Snackbar.error(m.network_device_add_error()); + } + }, }); const addButtonProps = useMemo( From cf4cd1d47aec7a3ac6f65d20aa3a90dac25d6f89 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:38:09 +0100 Subject: [PATCH 7/9] simplify error checking --- web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index fb4efc2910..2c25361f57 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -67,8 +67,8 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { }, onError: (e) => { console.error(e); - const status = (e as AxiosError).response?.status; - if (status === 400) { + const msg = (e as AxiosError<{ msg: string }>).response?.data?.msg; + if (msg?.toLowerCase().includes('network') && msg?.toLowerCase().includes('full')) { Snackbar.error(m.network_device_add_network_full()); } else { Snackbar.error(m.network_device_add_error()); From 9f7031e6bd07ca1f4eab76835d67a145eba81757 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:21:00 +0100 Subject: [PATCH 8/9] return enum --- crates/defguard_core/src/error.rs | 4 +++- crates/defguard_core/src/handlers/mod.rs | 13 +++++++++++++ .../defguard_core/src/handlers/network_devices.rs | 2 +- .../NetworkDevicesPage/NetworkDevicesTable.tsx | 11 ++++++++--- web/src/shared/api/types.ts | 7 +++++++ .../AddDeviceModalManualSetupStep.tsx | 5 ++--- 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index eb214840b1..dc457ca4b1 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -89,6 +89,8 @@ pub enum WebError { #[error(transparent)] #[schema(value_type=Object)] StaticIpError(#[from] StaticIpError), + #[error("Network full: {0}")] + NetworkFull(String), } impl From for WebError { @@ -122,7 +124,7 @@ impl From for WebError { DeviceError::DatabaseError(_) => Self::DbError(error.to_string()), DeviceError::NetworkIpAssignmentError(_) => Self::ModelError(error.to_string()), DeviceError::Unexpected(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR), - DeviceError::NetworkFull(_) => Self::BadRequest(error.to_string()), + DeviceError::NetworkFull(_) => Self::NetworkFull(error.to_string()), DeviceError::ModelError(_) => Self::ModelError(error.to_string()), } } diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index 857e486b0b..209aad06ab 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -56,6 +56,12 @@ pub mod wireguard; pub mod worker; pub(crate) mod yubikey; +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub enum WebErrorType { + NetworkFull, +} + pub static SESSION_COOKIE_NAME: &str = "defguard_session"; pub(crate) static SIGN_IN_COOKIE_NAME: &str = "defguard_sign_in"; pub(crate) const SIGN_IN_COOKIE_MAX_AGE: time::Duration = time::Duration::minutes(10); @@ -224,6 +230,13 @@ impl From for ApiResponse { error!(msg); ApiResponse::new(json!({"msg": msg}), StatusCode::BAD_REQUEST) } + WebError::NetworkFull(msg) => { + warn!(msg); + ApiResponse::new( + json!({"msg": msg, "type": WebErrorType::NetworkFull}), + StatusCode::BAD_REQUEST, + ) + } WebError::TemplateError(err) => { error!("Template error: {err}"); ApiResponse::new( diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index fdd6507845..d32bd05829 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -354,7 +354,7 @@ pub(crate) async fn find_available_ips( "Failed to find available IPs for new device in network {} ({:?})", network.name, network.address ); - return Err(WebError::BadRequest(format!( + return Err(WebError::NetworkFull(format!( "Network {} is full, no IP addresses available", network.name ))); diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index 2c25361f57..fabafcdde6 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -10,7 +10,12 @@ import { orderBy } from 'lodash-es'; import { useMemo } from 'react'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; -import { LocationMfaMode, type NetworkDevice } from '../../shared/api/types'; +import { + type ApiError, + LocationMfaMode, + type NetworkDevice, + WebErrorType, +} from '../../shared/api/types'; import { Badge } from '../../shared/defguard-ui/components/Badge/Badge'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import type { ButtonProps } from '../../shared/defguard-ui/components/Button/types'; @@ -67,8 +72,8 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { }, onError: (e) => { console.error(e); - const msg = (e as AxiosError<{ msg: string }>).response?.data?.msg; - if (msg?.toLowerCase().includes('network') && msg?.toLowerCase().includes('full')) { + const type = (e as AxiosError).response?.data?.type; + if (type === WebErrorType.NetworkFull) { Snackbar.error(m.network_device_add_network_full()); } else { Snackbar.error(m.network_device_add_error()); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index ef0fac4845..c5d19c5b5b 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -305,9 +305,16 @@ export interface MfaFinishResponse { user?: User; } +export const WebErrorType = { + NetworkFull: 'network_full', +} as const; + +export type WebErrorType = (typeof WebErrorType)[keyof typeof WebErrorType]; + export interface ApiError { msg?: string; message?: string; + type?: WebErrorType; } export interface AppInfoExceededLimits { diff --git a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx index 515153d67e..6a8dabadfb 100644 --- a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx +++ b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx @@ -10,7 +10,7 @@ import { useMutation } from '@tanstack/react-query'; import type { AxiosError } from 'axios'; import { useMemo } from 'react'; import api from '../../../../../api/api'; -import type { ApiError } from '../../../../../api/types'; +import { type ApiError, WebErrorType } from '../../../../../api/types'; import { SizedBox } from '../../../../../defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../../../defguard-ui/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../../../../defguard-ui/types'; @@ -109,8 +109,7 @@ export const AddDeviceModalManualSetupStep = () => { }, }); } else if ( - msg?.toLowerCase().includes('network') && - msg?.toLowerCase().includes('full') + (e as AxiosError).response?.data?.type === WebErrorType.NetworkFull ) { Snackbar.error(m.form_error_network_full()); } From 8d192c9d93c60713f3bd02269db1773a7f4dd809 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:10:14 +0100 Subject: [PATCH 9/9] add type in other way --- crates/defguard_core/src/handlers/mod.rs | 4 ++-- web/messages/en/api-error.json | 4 ++++ web/messages/en/form.json | 1 - web/messages/en/location.json | 1 - web/project.inlang/settings.json | 3 ++- .../pages/NetworkDevicesPage/NetworkDevicesTable.tsx | 8 ++++---- web/src/shared/api/apiErrorMessages.ts | 7 +++++++ web/src/shared/api/types.ts | 8 +++++--- .../AddDeviceModalManualSetupStep.tsx | 12 +++++++----- 9 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 web/messages/en/api-error.json create mode 100644 web/src/shared/api/apiErrorMessages.ts diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index 209aad06ab..7811bac66c 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -58,7 +58,7 @@ pub(crate) mod yubikey; #[derive(Serialize)] #[serde(rename_all = "snake_case")] -pub enum WebErrorType { +pub enum WebErrorCode { NetworkFull, } @@ -233,7 +233,7 @@ impl From for ApiResponse { WebError::NetworkFull(msg) => { warn!(msg); ApiResponse::new( - json!({"msg": msg, "type": WebErrorType::NetworkFull}), + json!({"msg": msg, "code": WebErrorCode::NetworkFull}), StatusCode::BAD_REQUEST, ) } diff --git a/web/messages/en/api-error.json b/web/messages/en/api-error.json new file mode 100644 index 0000000000..a049422d23 --- /dev/null +++ b/web/messages/en/api-error.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "api_error_network_full": "Network is full, no IP addresses available" +} diff --git a/web/messages/en/form.json b/web/messages/en/form.json index eb4224e11b..0775653714 100644 --- a/web/messages/en/form.json +++ b/web/messages/en/form.json @@ -24,7 +24,6 @@ "form_error_current_password": "Current password is incorrect.", "form_error_invalid_key": "Invalid key format", "form_error_key_exists": "Key already exists", - "form_error_network_full": "Network is full, no IP addresses available", "form_error_ip_invalid": "IP is invalid", "form_error_ip_reserved": "IP is not available", "password_form_check_title": "Your password must include:", diff --git a/web/messages/en/location.json b/web/messages/en/location.json index feecb083cd..2923ef87c2 100644 --- a/web/messages/en/location.json +++ b/web/messages/en/location.json @@ -10,7 +10,6 @@ "network_device_delete_success": "Network device deleted", "network_device_delete_failed": "Failed to delete network device", "network_device_add_error": "Failed to load network data for adding device", - "network_device_add_network_full": "Network is full, no IP addresses available for a new device", "location_edit_failed": "Failed to update location", "location_edit_addresses_rewrite_warning": "Changing the Gateway VPN IP address or netmask will automatically reassign any device addresses that fall outside the new network range to a randomly selected available IP address." } diff --git a/web/project.inlang/settings.json b/web/project.inlang/settings.json index 61308a6bc2..6fc3720e14 100644 --- a/web/project.inlang/settings.json +++ b/web/project.inlang/settings.json @@ -27,7 +27,8 @@ "./messages/{locale}/settings.json", "./messages/{locale}/gateway_wizard.json", "./messages/{locale}/initial_wizard.json", - "./messages/{locale}/migration_wizard.json" + "./messages/{locale}/migration_wizard.json", + "./messages/{locale}/api-error.json" ] } } diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index fabafcdde6..29df046ae1 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -10,11 +10,11 @@ import { orderBy } from 'lodash-es'; import { useMemo } from 'react'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; +import { getApiErrorMessage } from '../../shared/api/apiErrorMessages'; import { type ApiError, LocationMfaMode, type NetworkDevice, - WebErrorType, } from '../../shared/api/types'; import { Badge } from '../../shared/defguard-ui/components/Badge/Badge'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; @@ -72,9 +72,9 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { }, onError: (e) => { console.error(e); - const type = (e as AxiosError).response?.data?.type; - if (type === WebErrorType.NetworkFull) { - Snackbar.error(m.network_device_add_network_full()); + const code = (e as AxiosError).response?.data?.code; + if (code) { + Snackbar.error(getApiErrorMessage(code)); } else { Snackbar.error(m.network_device_add_error()); } diff --git a/web/src/shared/api/apiErrorMessages.ts b/web/src/shared/api/apiErrorMessages.ts new file mode 100644 index 0000000000..0a4a2dde68 --- /dev/null +++ b/web/src/shared/api/apiErrorMessages.ts @@ -0,0 +1,7 @@ +import { m } from '../../paraglide/messages'; +import type { ApiErrorMessageKey, WebErrorCode } from './types'; + +export function getApiErrorMessage(code: WebErrorCode): string { + const key: ApiErrorMessageKey = `api_error_${code}`; + return (m as Record string>)[key](); +} diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index c5d19c5b5b..9cc252d782 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -305,16 +305,18 @@ export interface MfaFinishResponse { user?: User; } -export const WebErrorType = { +export const WebErrorCode = { NetworkFull: 'network_full', } as const; -export type WebErrorType = (typeof WebErrorType)[keyof typeof WebErrorType]; +export type WebErrorCode = (typeof WebErrorCode)[keyof typeof WebErrorCode]; + +export type ApiErrorMessageKey = `api_error_${WebErrorCode}`; export interface ApiError { msg?: string; message?: string; - type?: WebErrorType; + code?: WebErrorCode; } export interface AppInfoExceededLimits { diff --git a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx index 6a8dabadfb..868db1e14d 100644 --- a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx +++ b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx @@ -10,7 +10,8 @@ import { useMutation } from '@tanstack/react-query'; import type { AxiosError } from 'axios'; import { useMemo } from 'react'; import api from '../../../../../api/api'; -import { type ApiError, WebErrorType } from '../../../../../api/types'; +import { getApiErrorMessage } from '../../../../../api/apiErrorMessages'; +import type { ApiError } from '../../../../../api/types'; import { SizedBox } from '../../../../../defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../../../defguard-ui/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../../../../defguard-ui/types'; @@ -108,10 +109,11 @@ export const AddDeviceModalManualSetupStep = () => { }, }, }); - } else if ( - (e as AxiosError).response?.data?.type === WebErrorType.NetworkFull - ) { - Snackbar.error(m.form_error_network_full()); + } else { + const code = (e as AxiosError).response?.data?.code; + if (code) { + Snackbar.error(getApiErrorMessage(code)); + } } });