diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 4e9a02a369..ee9d2ddaac 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -10,6 +10,10 @@ "misc_secret": "Secret", "misc_active": "Active", "misc_disabled": "Disabled", + "license_business_required": "Available in Business plan.", + "license_upgrade_business_tooltip": "This feature is part of a paid plan.\nUpgrade to Business to activate it.", + "license_enterprise_required": "Available in Enterprise plan.", + "license_upgrade_to_unlock": "Upgrade to unlock.", "controls_connect": "Connect", "controls_search": "Search", "controls_accept": "Accept", diff --git a/web/messages/en/location.json b/web/messages/en/location.json index 7b7bd2fd01..cd02cae668 100644 --- a/web/messages/en/location.json +++ b/web/messages/en/location.json @@ -1,6 +1,11 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "location_delete_success": "Location deleted", + "add_location_firewall_no_license_subtitle": "Firewall is available only in Business and Enterprise plans. You can still create location now and activate Firewall later by upgrading your plan.", + "add_location_internal_vpn_allowed_ips_from_firewall_rules": "Allowed IPs are generated automatically from firewall rules", "add_location_mfa_disabled_title": "Do not enforce MFA", + "add_location_mfa_internal_title": "Internal Defguard Multi-Factor Authentication", + "add_location_mfa_internal_content": "Uses the MFA methods configured in your Defguard profile.", + "add_location_mfa_external_title": "External Identity Provider Authentication", + "add_location_mfa_external_content": "Requires configuring an external identity provider in the settings, such as Google, Microsoft Entra ID, Okta, or JumpCloud.", "location_delete_success": "Location deleted", "location_delete_failed": "Failed to delete location", "location_edit_failed": "Failed to update location", "location_edit_failed_has_devices": "Gateway VPN IP address and netmask can’t be changed while devices are active on this network. To proceed, remove all devices from the current network first." diff --git a/web/src/pages/AddLocationPage/assets/business-feature-icon.png b/web/src/pages/AddLocationPage/assets/business-feature-icon.png new file mode 100644 index 0000000000..bbd9ef4337 Binary files /dev/null and b/web/src/pages/AddLocationPage/assets/business-feature-icon.png differ diff --git a/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx index 39323ad6c9..53b61ba452 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationFirewallStep.tsx @@ -1,11 +1,12 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { cloneDeep, omit } from 'lodash-es'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { m } from '../../../paraglide/messages'; import api from '../../../shared/api/api'; import { ActionCard } from '../../../shared/components/ActionCard/ActionCard'; import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; +import { externalLink } from '../../../shared/constants'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { Checkbox } from '../../../shared/defguard-ui/components/Checkbox/Checkbox'; import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; @@ -13,7 +14,11 @@ import { ModalControls } from '../../../shared/defguard-ui/components/ModalContr import { Radio } from '../../../shared/defguard-ui/components/Radio/Radio'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; +import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; +import { getLicenseInfoQueryOptions } from '../../../shared/query'; +import { canUseBusinessFeature } from '../../../shared/utils/license'; import { useGatewayWizardStore } from '../../GatewaySetupPage/useGatewayWizardStore'; +import businessFeatureCardImage from '../assets/business-feature-icon.png'; import actionCardImage from '../assets/gateway-setup-action-card.png'; import { AddLocationPageStep } from '../types'; import { useAddLocationStore } from '../useAddLocationStore'; @@ -25,6 +30,13 @@ export const AddLocationFirewallStep = () => { const [state, setState] = useState('disable'); const navigate = useNavigate(); + const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions); + const canUseFeature = useMemo(() => { + if (licenseInfo === undefined) return undefined; + return canUseBusinessFeature(licenseInfo).result; + }, [licenseInfo]); + const firewallLocked = isPresent(canUseFeature) && !canUseFeature; + const { mutate, isPending } = useMutation({ mutationFn: api.location.addLocation, meta: { @@ -75,12 +87,31 @@ export const AddLocationFirewallStep = () => { return ( + {firewallLocked && ( + <> + + +