diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index 290665739..2e128fd48 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -8,6 +8,8 @@ "modal_selection_title": "Select items", "modal_delete_gateway_title": "Delete Gateway", "modal_delete_gateway_body": "Deleting gateway {name} may cause location {locationName} to stop working if it is the only Gateway connected to this location or if other connected Gateways are currently unavailable. This action cannot be undone.", + "modal_delete_gateway_disabled_body": "Gateway **{name}** is currently **disabled**. Deleting it now will make it permanently unusable. Are you sure you want to proceed?", + "modal_delete_gateway_disconnected_body": "Gateway **{name}** is currently **disconnected**. Deleting it now will make it permanently unusable. Are you sure you want to proceed?", "modal_delete_location_title": "Delete location", "modal_delete_location_body": "Deleting location {name} will also delete related gateways. This action cannot be undone.", "modal_add_location_title": "Select location type", diff --git a/web/src/pages/EditGatewayPage/EditGatewayPage.tsx b/web/src/pages/EditGatewayPage/EditGatewayPage.tsx index 73f69686c..9f37eaf85 100644 --- a/web/src/pages/EditGatewayPage/EditGatewayPage.tsx +++ b/web/src/pages/EditGatewayPage/EditGatewayPage.tsx @@ -52,6 +52,29 @@ const EditGatewayForm = ({ gateway }: { gateway: Gateway }) => { getLocationQueryOptions(gateway.location_id), ); + const isConnected = useMemo(() => { + if (gateway.connected_at == null) { + return false; + } + if (gateway.disconnected_at == null) { + return true; + } + return gateway.connected_at >= gateway.disconnected_at; + }, [gateway.connected_at, gateway.disconnected_at]); + + const getDeleteModalContent = () => { + if (!gateway.enabled) { + return m.modal_delete_gateway_disabled_body({ name: gateway.name }); + } + if (!isConnected) { + return m.modal_delete_gateway_disconnected_body({ name: gateway.name }); + } + return m.modal_delete_gateway_body({ + name: gateway.name, + locationName: location.name, + }); + }; + const { mutateAsync: editGateway } = useMutation({ mutationFn: api.gateway.editGateway, meta: { @@ -167,10 +190,7 @@ const EditGatewayForm = ({ gateway }: { gateway: Gateway }) => { onClick: () => { openModal(ModalName.ConfirmAction, { title: m.modal_delete_gateway_title(), - contentMd: m.modal_delete_gateway_body({ - name: gateway.name, - locationName: location.name, - }), + contentMd: getDeleteModalContent(), actionPromise: () => api.gateway.deleteGateway(gateway.id), invalidateKeys: [['gateway'], ['network']], submitProps: { text: m.gateway_edit_delete(), variant: 'critical' }, diff --git a/web/src/pages/LocationsPage/components/GatewaysTable.tsx b/web/src/pages/LocationsPage/components/GatewaysTable.tsx index f675839ef..dd84d49ea 100644 --- a/web/src/pages/LocationsPage/components/GatewaysTable.tsx +++ b/web/src/pages/LocationsPage/components/GatewaysTable.tsx @@ -198,6 +198,20 @@ export const GatewaysTable = () => { enableResizing: false, cell: (info) => { const rowData = info.row.original; + + const getDeleteModalContent = () => { + if (!rowData.enabled) { + return m.modal_delete_gateway_disabled_body({ name: rowData.name }); + } + if (!rowData.connected) { + return m.modal_delete_gateway_disconnected_body({ name: rowData.name }); + } + return m.modal_delete_gateway_body({ + name: rowData.name, + locationName: rowData.location_name, + }); + }; + const menuItems: MenuItemsGroup[] = [ { items: [ @@ -235,10 +249,7 @@ export const GatewaysTable = () => { onClick: () => { openModal(ModalName.ConfirmAction, { title: m.modal_delete_gateway_title(), - contentMd: m.modal_delete_gateway_body({ - name: rowData.name, - locationName: rowData.location_name, - }), + contentMd: getDeleteModalContent(), actionPromise: () => api.gateway.deleteGateway(rowData.id), invalidateKeys: [['gateway'], ['network']], submitProps: { text: m.controls_delete(), variant: 'critical' }, diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSslConfigStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSslConfigStep.tsx index 887a783a9..ce466af99 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSslConfigStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSslConfigStep.tsx @@ -71,8 +71,8 @@ export const MigrationWizardExternalUrlSslConfigStep = () => { }, []); const { data: sslInfoData } = useQuery({ - queryKey: ['external_ssl_info'], - queryFn: () => api.initial_setup.getExternalSslInfo(), + queryKey: ['migration', 'external_ssl_info'], + queryFn: () => api.migration.getExternalSslInfo(), enabled: sslType === 'defguard_ca', select: (response) => response.data, }); diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSslConfigStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSslConfigStep.tsx index b02fac48e..0210b134b 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSslConfigStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSslConfigStep.tsx @@ -28,8 +28,8 @@ export const MigrationWizardInternalUrlSslConfigStep = () => { }, []); const { data: sslInfoData } = useQuery({ - queryKey: ['internal_ssl_info'], - queryFn: () => api.initial_setup.getInternalSslInfo(), + queryKey: ['migration', 'internal_ssl_info'], + queryFn: () => api.migration.getInternalSslInfo(), enabled: sslType === 'defguard_ca', select: (response) => response.data, }); diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx index 2ac8f449b..689f82a06 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx @@ -40,6 +40,23 @@ const discriminatedSchema = z.discriminatedUnion('directory_sync_enabled', [ syncSchema, ]); +const validationSchema = syncSchema + .omit({ directory_sync_interval: true }) + .extend({ directory_sync_interval: z.number() }) + .superRefine((val, ctx) => { + if ( + val.directory_sync_enabled && + (typeof val.directory_sync_interval !== 'number' || + val.directory_sync_interval < 60) + ) { + ctx.addIssue({ + path: ['directory_sync_interval'], + code: 'custom', + message: m.form_error_min({ value: 60 }), + }); + } + }); + type FormFields = z.infer; export const EditMicrosoftProviderForm = ({ @@ -77,8 +94,8 @@ export const EditMicrosoftProviderForm = ({ defaultValues, validationLogic: formChangeLogic, validators: { - onSubmit: syncSchema, - onChange: syncSchema, + onSubmit: validationSchema, + onChange: validationSchema, }, onSubmit: async ({ value }) => { const base_url = formatMicrosoftBaseUrl(value.microsoftTenantId); diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditOktaProviderForm.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditOktaProviderForm.tsx index c23d50ecd..88dae7d73 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditOktaProviderForm.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditOktaProviderForm.tsx @@ -32,6 +32,31 @@ const discriminatedSchema = z.discriminatedUnion('directory_sync_enabled', [ syncSchema, ]); +const validationSchema = syncSchema + .omit({ okta_dirsync_client_id: true, okta_private_jwk: true }) + .extend({ + okta_dirsync_client_id: z.string(), + okta_private_jwk: z.string(), + }) + .superRefine((val, ctx) => { + if (val.directory_sync_enabled) { + if (val.okta_dirsync_client_id.trim().length === 0) { + ctx.addIssue({ + path: ['okta_dirsync_client_id'], + code: 'custom', + message: m.form_error_required(), + }); + } + if (val.okta_private_jwk.trim().length === 0) { + ctx.addIssue({ + path: ['okta_private_jwk'], + code: 'custom', + message: m.form_error_required(), + }); + } + } + }); + type FormFields = z.infer; export const EditOktaProviderForm = ({ @@ -61,8 +86,8 @@ export const EditOktaProviderForm = ({ defaultValues, validationLogic: formChangeLogic, validators: { - onSubmit: syncSchema, - onChange: syncSchema, + onSubmit: validationSchema, + onChange: validationSchema, }, onSubmit: async ({ value }) => { await onSubmit(value); diff --git a/web/src/shared/components/GatewaysStatusBadge/GatewaysStatusBadge.tsx b/web/src/shared/components/GatewaysStatusBadge/GatewaysStatusBadge.tsx index c4d69b17c..be4b1bf86 100644 --- a/web/src/shared/components/GatewaysStatusBadge/GatewaysStatusBadge.tsx +++ b/web/src/shared/components/GatewaysStatusBadge/GatewaysStatusBadge.tsx @@ -10,7 +10,6 @@ import { useFloating, useInteractions, } from '@floating-ui/react'; -import { useMutation } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import { type HTMLProps, useMemo, useState } from 'react'; @@ -26,7 +25,10 @@ import { Icon } from '../../defguard-ui/components/Icon'; import type { IconKindValue } from '../../defguard-ui/components/Icon/icon-types'; import { InteractionBox } from '../../defguard-ui/components/InteractionBox/InteractionBox'; import { SizedBox } from '../../defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../defguard-ui/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../defguard-ui/types'; +import { openModal } from '../../hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../hooks/modalControls/modalTypes'; import './style.scss'; type Status = 'all' | 'none' | 'some'; @@ -151,13 +153,6 @@ const FloatingMenu = ({ const disconnected = useMemo(() => status.filter((gw) => !gw.connected), [status]); const navigate = useNavigate(); - const { mutate: removeGw } = useMutation({ - mutationFn: api.gateway.deleteGateway, - meta: { - invalidate: [['gateway'], ['network']], - }, - }); - return (
{connected.length > 0 && ( @@ -198,7 +193,29 @@ const FloatingMenu = ({ icon="close" iconSize={20} onClick={() => { - removeGw(gw.id); + const getDeleteModalContent = () => { + if (!gw.enabled) { + return m.modal_delete_gateway_disabled_body({ name: gw.name }); + } + if (!gw.connected) { + return m.modal_delete_gateway_disconnected_body({ + name: gw.name, + }); + } + return m.modal_delete_gateway_body({ + name: gw.name, + locationName: gw.location_name, + }); + }; + openModal(ModalName.ConfirmAction, { + title: m.modal_delete_gateway_title(), + contentMd: getDeleteModalContent(), + actionPromise: () => api.gateway.deleteGateway(gw.id), + invalidateKeys: [['gateway'], ['network']], + submitProps: { text: m.controls_delete(), variant: 'critical' }, + onSuccess: () => Snackbar.default(m.gateway_delete_success()), + onError: () => Snackbar.error(m.gateway_delete_failed()), + }); }} />