From cfb0e1c42bda75c14eb68dc8485b835fbeb62593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 08:57:58 +0100 Subject: [PATCH 01/19] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 52b9121c40..77910f7a75 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ ladle-build result/ .aider* .env +.zellij_layout.kdl From 32d87a50c8e82570df65aba2e2e84181e7eedf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 12:44:23 +0100 Subject: [PATCH 02/19] add icon in navbar for pending ACL changes --- .../components/Navigation/Navigation.tsx | 78 ++++++++++++++++--- .../shared/components/Navigation/style.scss | 17 ++-- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/web/src/shared/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index 68651c06fd..c0f439e016 100644 --- a/web/src/shared/components/Navigation/Navigation.tsx +++ b/web/src/shared/components/Navigation/Navigation.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react'; import { m } from '../../../paraglide/messages'; +import { CounterLabel } from '../../defguard-ui/components/CounterLabel/CounterLabel'; import { Icon, IconKind } from '../../defguard-ui/components/Icon'; import type { IconKindValue } from '../../defguard-ui/components/Icon/icon-types'; import { IconButton } from '../../defguard-ui/components/IconButton/IconButton'; @@ -16,7 +17,12 @@ import { TooltipProvider } from '../../defguard-ui/providers/tooltip/TooltipCont import { TooltipTrigger } from '../../defguard-ui/providers/tooltip/TooltipTrigger'; import { isPresent } from '../../defguard-ui/utils/isPresent'; import { useTheme } from '../../hooks/theme/useTheme'; -import { getLicenseInfoQueryOptions } from '../../query'; +import { + getAliasesCountQueryOptions, + getDestinationsCountQueryOptions, + getLicenseInfoQueryOptions, + getRulesCountQueryOptions, +} from '../../query'; import { canUseBusinessFeature } from '../../utils/license'; interface NavGroupProps { @@ -34,6 +40,7 @@ interface NavItemProps { licenseTier?: LicenseTierValue; license?: LicenseInfo | null; testId?: string; + pendingCount?: number; } const navigationConfig: NavGroupProps[] = [ @@ -160,6 +167,37 @@ export const Navigation = () => { enabled: isAdmin, }); + const { data: rulesCount } = useQuery({ + ...getRulesCountQueryOptions, + enabled: isAdmin, + }); + + const { data: destinationsCount } = useQuery({ + ...getDestinationsCountQueryOptions, + enabled: isAdmin, + }); + + const { data: aliasesCount } = useQuery({ + ...getAliasesCountQueryOptions, + enabled: isAdmin, + }); + + const navigationGroups = useMemo(() => { + const pendingCounts = { + rules: rulesCount?.pending, + destinations: destinationsCount?.pending, + aliases: aliasesCount?.pending, + }; + + return navigationConfig.map((group) => ({ + ...group, + items: group.items.map((item) => ({ + ...item, + pendingCount: pendingCounts[item.id as keyof typeof pendingCounts], + })), + })); + }, [aliasesCount, destinationsCount, rulesCount]); + if (!isAdmin || !isOpen) return null; return (
@@ -177,7 +215,7 @@ export const Navigation = () => {
- {navigationConfig.map((group) => ( + {navigationGroups.map((group) => ( ))}
@@ -212,7 +250,15 @@ const NavGroup = ({ items, label, licenseInfo }: NavGroupProps) => { ); }; -const NavItem = ({ icon, link, label, testId, license, licenseTier }: NavItemProps) => { +const NavItem = ({ + icon, + link, + label, + testId, + license, + licenseTier, + pendingCount, +}: NavItemProps) => { const showLock = useMemo(() => { if (licenseTier === undefined) { return isPresent(licenseTier); @@ -229,20 +275,28 @@ const NavItem = ({ icon, link, label, testId, license, licenseTier }: NavItemPro return false; }, [license, licenseTier]); + const showPending = isPresent(pendingCount) && pendingCount > 0; + const showRight = showPending || (showLock && isPresent(licenseTier)); + return ( {label} - {showLock && isPresent(licenseTier) && ( + {showRight && (
- - - - - -

{`This is ${licenseTier ?? 'Unknown tier'} feature`}

-
-
+ {showPending && ( + + )} + {showLock && isPresent(licenseTier) && ( + + + + + +

{`This is ${licenseTier ?? 'Unknown tier'} feature`}

+
+
+ )}
)} diff --git a/web/src/shared/components/Navigation/style.scss b/web/src/shared/components/Navigation/style.scss index 3ccd7eb55e..7ee7ef0d6a 100644 --- a/web/src/shared/components/Navigation/style.scss +++ b/web/src/shared/components/Navigation/style.scss @@ -14,7 +14,7 @@ justify-content: flex-start; min-height: 100dvh; - & > .top { + &>.top { display: flex; flex-flow: row nowrap; align-items: center; @@ -23,7 +23,7 @@ padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-lg); width: 100%; - & > .control { + &>.control { margin-left: auto; .icon[icon-kind='hamburger'] path { @@ -32,7 +32,7 @@ } } - & > .groups { + &>.groups { display: flex; flex-flow: column; row-gap: var(--spacing-lg); @@ -41,7 +41,7 @@ width: 100%; } - & > .bottom { + &>.bottom { width: 100%; margin-top: auto; box-sizing: border-box; @@ -59,7 +59,7 @@ .navigation .nav-group { user-select: none; - & > .track { + &>.track { display: flex; flex-flow: row nowrap; align-items: center; @@ -80,7 +80,7 @@ .fold-content { padding-top: var(--spacing-xs); - & > .items { + &>.items { display: flex; flex-flow: column; row-gap: var(--spacing-xs); @@ -115,7 +115,7 @@ --color: var(--fg-action); } - & > .icon path { + &>.icon path { fill: var(--color); @include animate(fill); @@ -132,5 +132,6 @@ flex-flow: column; align-items: center; justify-content: center; + row-gap: var(--spacing-xxs); } -} +} \ No newline at end of file From 72cd4bc98ac729084e1b42853317457278e952ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 13:27:44 +0100 Subject: [PATCH 03/19] add warning badge to pending tabs --- web/src/pages/AliasesPage/AliasesPage.tsx | 9 +++++++-- web/src/pages/DestinationsPage/DestinationsPage.tsx | 11 +++++++---- web/src/pages/RulesPage/RulesPage.tsx | 9 ++++++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/web/src/pages/AliasesPage/AliasesPage.tsx b/web/src/pages/AliasesPage/AliasesPage.tsx index 4f0480a87b..2adb84b25a 100644 --- a/web/src/pages/AliasesPage/AliasesPage.tsx +++ b/web/src/pages/AliasesPage/AliasesPage.tsx @@ -18,6 +18,10 @@ export const AliasesPage = () => { AclDeploymentState.Applied, ); + const pendingCount = aliasesCount?.pending ?? 0; + const pendingTitle = pendingCount ? `Pending (${pendingCount})` : 'Pending'; + const pendingBadgeText = pendingCount > 0 ? '!' : undefined; + const tabs = useMemo( (): TabsItem[] => [ { @@ -32,10 +36,11 @@ export const AliasesPage = () => { onClick: () => { setActiveTab(AclDeploymentState.Modified); }, - title: aliasesCount?.pending ? `Pending (${aliasesCount.pending})` : 'Pending', + title: pendingTitle, + badgeText: pendingBadgeText, }, ], - [activeTab, aliasesCount], + [activeTab, pendingBadgeText, pendingTitle], ); return ( diff --git a/web/src/pages/DestinationsPage/DestinationsPage.tsx b/web/src/pages/DestinationsPage/DestinationsPage.tsx index fc6ccbfd7c..5721068f85 100644 --- a/web/src/pages/DestinationsPage/DestinationsPage.tsx +++ b/web/src/pages/DestinationsPage/DestinationsPage.tsx @@ -17,6 +17,10 @@ export const DestinationsPage = () => { AclDeploymentState.Applied, ); + const pendingCount = destinationsCount?.pending ?? 0; + const pendingTitle = pendingCount ? `Pending (${pendingCount})` : 'Pending'; + const pendingBadgeText = pendingCount > 0 ? '!' : undefined; + const tabs = useMemo( (): TabsItem[] => [ { @@ -31,12 +35,11 @@ export const DestinationsPage = () => { onClick: () => { setActiveTab(AclDeploymentState.Modified); }, - title: destinationsCount?.pending - ? `Pending (${destinationsCount.pending})` - : 'Pending', + title: pendingTitle, + badgeText: pendingBadgeText, }, ], - [activeTab, destinationsCount], + [activeTab, pendingBadgeText, pendingTitle], ); return ( diff --git a/web/src/pages/RulesPage/RulesPage.tsx b/web/src/pages/RulesPage/RulesPage.tsx index a4024ac889..d97ea9eb5f 100644 --- a/web/src/pages/RulesPage/RulesPage.tsx +++ b/web/src/pages/RulesPage/RulesPage.tsx @@ -17,10 +17,12 @@ export const RulesPage = () => { const { data: rulesCount } = useQuery(getRulesCountQueryOptions); + const pendingCount = rulesCount?.pending ?? 0; const pendingTabTitle = useMemo( - () => `Pending${rulesCount?.pending ? ` (${rulesCount.pending})` : ''}`, - [rulesCount], + () => `Pending${pendingCount ? ` (${pendingCount})` : ''}`, + [pendingCount], ); + const pendingBadgeText = pendingCount > 0 ? '!' : undefined; const tabs = useMemo( (): TabsItem[] => [ @@ -33,13 +35,14 @@ export const RulesPage = () => { }, { title: pendingTabTitle, + badgeText: pendingBadgeText, active: activeTab === RulesPageTab.Pending, onClick: () => { setActiveTab(RulesPageTab.Pending); }, }, ], - [activeTab, pendingTabTitle], + [activeTab, pendingBadgeText, pendingTabTitle], ); return ( From 00f5ac4cb9d95e8b037a38cb6108aa5a044f2ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 13:49:49 +0100 Subject: [PATCH 04/19] fix counter styling --- web/src/shared/components/Navigation/Navigation.tsx | 2 +- web/src/shared/components/Navigation/style.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/shared/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index c0f439e016..449c71aaa1 100644 --- a/web/src/shared/components/Navigation/Navigation.tsx +++ b/web/src/shared/components/Navigation/Navigation.tsx @@ -285,7 +285,7 @@ const NavItem = ({ {showRight && (
{showPending && ( - + )} {showLock && isPresent(licenseTier) && ( diff --git a/web/src/shared/components/Navigation/style.scss b/web/src/shared/components/Navigation/style.scss index 7ee7ef0d6a..6798a0c104 100644 --- a/web/src/shared/components/Navigation/style.scss +++ b/web/src/shared/components/Navigation/style.scss @@ -121,7 +121,7 @@ @include animate(fill); } - span { + &>span { font: var(--t-body-sm-500); color: inherit; } From 946cd421e8b5ab7c453ac1fc98b4739fe96c7d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 15:15:05 +0100 Subject: [PATCH 05/19] formatting --- .../components/Navigation/Navigation.tsx | 4 +--- .../shared/components/Navigation/style.scss | 18 +++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/web/src/shared/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index 449c71aaa1..327f1d74b7 100644 --- a/web/src/shared/components/Navigation/Navigation.tsx +++ b/web/src/shared/components/Navigation/Navigation.tsx @@ -284,9 +284,7 @@ const NavItem = ({ {label} {showRight && (
- {showPending && ( - - )} + {showPending && } {showLock && isPresent(licenseTier) && ( diff --git a/web/src/shared/components/Navigation/style.scss b/web/src/shared/components/Navigation/style.scss index 6798a0c104..5b89b3c5f2 100644 --- a/web/src/shared/components/Navigation/style.scss +++ b/web/src/shared/components/Navigation/style.scss @@ -14,7 +14,7 @@ justify-content: flex-start; min-height: 100dvh; - &>.top { + & > .top { display: flex; flex-flow: row nowrap; align-items: center; @@ -23,7 +23,7 @@ padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-lg); width: 100%; - &>.control { + & > .control { margin-left: auto; .icon[icon-kind='hamburger'] path { @@ -32,7 +32,7 @@ } } - &>.groups { + & > .groups { display: flex; flex-flow: column; row-gap: var(--spacing-lg); @@ -41,7 +41,7 @@ width: 100%; } - &>.bottom { + & > .bottom { width: 100%; margin-top: auto; box-sizing: border-box; @@ -59,7 +59,7 @@ .navigation .nav-group { user-select: none; - &>.track { + & > .track { display: flex; flex-flow: row nowrap; align-items: center; @@ -80,7 +80,7 @@ .fold-content { padding-top: var(--spacing-xs); - &>.items { + & > .items { display: flex; flex-flow: column; row-gap: var(--spacing-xs); @@ -115,13 +115,13 @@ --color: var(--fg-action); } - &>.icon path { + & > .icon path { fill: var(--color); @include animate(fill); } - &>span { + & > span { font: var(--t-body-sm-500); color: inherit; } @@ -134,4 +134,4 @@ justify-content: center; row-gap: var(--spacing-xxs); } -} \ No newline at end of file +} From 1e734824c667bd5d4cb1b30b81088be7c4777640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 15:15:39 +0100 Subject: [PATCH 06/19] add icons in pending tabs --- web/src/pages/AliasesPage/AliasesPage.tsx | 7 ++++--- web/src/pages/DestinationsPage/DestinationsPage.tsx | 7 ++++--- web/src/pages/RulesPage/RulesPage.tsx | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/web/src/pages/AliasesPage/AliasesPage.tsx b/web/src/pages/AliasesPage/AliasesPage.tsx index 2adb84b25a..35a2a2440e 100644 --- a/web/src/pages/AliasesPage/AliasesPage.tsx +++ b/web/src/pages/AliasesPage/AliasesPage.tsx @@ -4,6 +4,7 @@ import { Suspense, useMemo, useState } from 'react'; import { AclDeploymentState, type AclDeploymentStateValue } from '../../shared/api/types'; import { Page } from '../../shared/components/Page/Page'; import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton'; +import { IconKind } from '../../shared/defguard-ui/components/Icon'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout'; @@ -20,7 +21,7 @@ export const AliasesPage = () => { const pendingCount = aliasesCount?.pending ?? 0; const pendingTitle = pendingCount ? `Pending (${pendingCount})` : 'Pending'; - const pendingBadgeText = pendingCount > 0 ? '!' : undefined; + const pendingIcon = pendingCount > 0 ? IconKind.AttentionFilled : undefined; const tabs = useMemo( (): TabsItem[] => [ @@ -37,10 +38,10 @@ export const AliasesPage = () => { setActiveTab(AclDeploymentState.Modified); }, title: pendingTitle, - badgeText: pendingBadgeText, + icon: pendingIcon, }, ], - [activeTab, pendingBadgeText, pendingTitle], + [activeTab, pendingIcon, pendingTitle], ); return ( diff --git a/web/src/pages/DestinationsPage/DestinationsPage.tsx b/web/src/pages/DestinationsPage/DestinationsPage.tsx index 5721068f85..7ffb0ff54c 100644 --- a/web/src/pages/DestinationsPage/DestinationsPage.tsx +++ b/web/src/pages/DestinationsPage/DestinationsPage.tsx @@ -3,6 +3,7 @@ import { Suspense, useMemo, useState } from 'react'; import { AclDeploymentState, type AclDeploymentStateValue } from '../../shared/api/types'; import { Page } from '../../shared/components/Page/Page'; import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton'; +import { IconKind } from '../../shared/defguard-ui/components/Icon'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout'; @@ -19,7 +20,7 @@ export const DestinationsPage = () => { const pendingCount = destinationsCount?.pending ?? 0; const pendingTitle = pendingCount ? `Pending (${pendingCount})` : 'Pending'; - const pendingBadgeText = pendingCount > 0 ? '!' : undefined; + const pendingIcon = pendingCount > 0 ? IconKind.AttentionFilled : undefined; const tabs = useMemo( (): TabsItem[] => [ @@ -36,10 +37,10 @@ export const DestinationsPage = () => { setActiveTab(AclDeploymentState.Modified); }, title: pendingTitle, - badgeText: pendingBadgeText, + icon: pendingIcon, }, ], - [activeTab, pendingBadgeText, pendingTitle], + [activeTab, pendingIcon, pendingTitle], ); return ( diff --git a/web/src/pages/RulesPage/RulesPage.tsx b/web/src/pages/RulesPage/RulesPage.tsx index d97ea9eb5f..80c7330a5e 100644 --- a/web/src/pages/RulesPage/RulesPage.tsx +++ b/web/src/pages/RulesPage/RulesPage.tsx @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { Suspense, useMemo, useState } from 'react'; import { Page } from '../../shared/components/Page/Page'; import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton'; +import { IconKind } from '../../shared/defguard-ui/components/Icon'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; @@ -22,7 +23,7 @@ export const RulesPage = () => { () => `Pending${pendingCount ? ` (${pendingCount})` : ''}`, [pendingCount], ); - const pendingBadgeText = pendingCount > 0 ? '!' : undefined; + const pendingIcon = pendingCount > 0 ? IconKind.AttentionFilled : undefined; const tabs = useMemo( (): TabsItem[] => [ @@ -35,14 +36,14 @@ export const RulesPage = () => { }, { title: pendingTabTitle, - badgeText: pendingBadgeText, + icon: pendingIcon, active: activeTab === RulesPageTab.Pending, onClick: () => { setActiveTab(RulesPageTab.Pending); }, }, ], - [activeTab, pendingBadgeText, pendingTabTitle], + [activeTab, pendingIcon, pendingTabTitle], ); return ( From f1080885801e67ede5f6fa9190f30dc28e7ce15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 15:40:17 +0100 Subject: [PATCH 07/19] add info notification about new pending objects --- web/src/pages/CEAliasPage/CEAliasPage.tsx | 5 +++++ web/src/pages/CEDestinationPage/CEDestinationPage.tsx | 2 ++ web/src/pages/CERulePage/CERulePage.tsx | 2 ++ 3 files changed, 9 insertions(+) diff --git a/web/src/pages/CEAliasPage/CEAliasPage.tsx b/web/src/pages/CEAliasPage/CEAliasPage.tsx index e131d36225..1a62d6fcfb 100644 --- a/web/src/pages/CEAliasPage/CEAliasPage.tsx +++ b/web/src/pages/CEAliasPage/CEAliasPage.tsx @@ -17,6 +17,7 @@ import { Controls } from '../../shared/components/Controls/Controls'; import { DescriptionBlock } from '../../shared/components/DescriptionBlock/DescriptionBlock'; import { EditPage } from '../../shared/components/EditPage/EditPage'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; @@ -138,8 +139,12 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => { }; if (isPresent(alias)) { await editAlias({ ...toSend, id: alias.id }); + Snackbar.success('Alias modified'); + Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); } else { await addAlias(toSend); + Snackbar.success('Alias added'); + Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); } router.history.back(); }, diff --git a/web/src/pages/CEDestinationPage/CEDestinationPage.tsx b/web/src/pages/CEDestinationPage/CEDestinationPage.tsx index d044f0443a..dd334fb953 100644 --- a/web/src/pages/CEDestinationPage/CEDestinationPage.tsx +++ b/web/src/pages/CEDestinationPage/CEDestinationPage.tsx @@ -81,6 +81,7 @@ export const CEDestinationPage = ({ destination }: Props) => { mutationFn: api.acl.destination.addDestination, onSuccess: () => { Snackbar.success('Destination added'); + Snackbar.default('Destinations added to Pending tab and awaiting deployment.'); }, onError: (e) => { Snackbar.error('Error occurred'); @@ -95,6 +96,7 @@ export const CEDestinationPage = ({ destination }: Props) => { mutationFn: api.acl.destination.editDestination, onSuccess: () => { Snackbar.success('Destination modified'); + Snackbar.default('Destinations added to Pending tab and awaiting deployment.'); }, onError: (e) => { Snackbar.error('Error occurred'); diff --git a/web/src/pages/CERulePage/CERulePage.tsx b/web/src/pages/CERulePage/CERulePage.tsx index 526e187ccc..b8bf65ab05 100644 --- a/web/src/pages/CERulePage/CERulePage.tsx +++ b/web/src/pages/CERulePage/CERulePage.tsx @@ -152,6 +152,7 @@ const Content = ({ rule: initialRule }: Props) => { }, onSuccess: () => { Snackbar.success('Rule added'); + Snackbar.default('Rules added to Pending tab and awaiting deployment.'); router.history.back(); }, }); @@ -163,6 +164,7 @@ const Content = ({ rule: initialRule }: Props) => { }, onSuccess: () => { Snackbar.success('Rule changed'); + Snackbar.default('Rules added to Pending tab and awaiting deployment.'); router.history.back(); }, }); From b537e3e87e0d5271b5b6a5d71b8787f7b4cda147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 2 Mar 2026 15:45:31 +0100 Subject: [PATCH 08/19] remove success snackbars --- web/src/pages/CEAliasPage/CEAliasPage.tsx | 2 -- web/src/pages/CEDestinationPage/CEDestinationPage.tsx | 2 -- web/src/pages/CERulePage/CERulePage.tsx | 2 -- 3 files changed, 6 deletions(-) diff --git a/web/src/pages/CEAliasPage/CEAliasPage.tsx b/web/src/pages/CEAliasPage/CEAliasPage.tsx index 1a62d6fcfb..2a69879de3 100644 --- a/web/src/pages/CEAliasPage/CEAliasPage.tsx +++ b/web/src/pages/CEAliasPage/CEAliasPage.tsx @@ -139,11 +139,9 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => { }; if (isPresent(alias)) { await editAlias({ ...toSend, id: alias.id }); - Snackbar.success('Alias modified'); Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); } else { await addAlias(toSend); - Snackbar.success('Alias added'); Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); } router.history.back(); diff --git a/web/src/pages/CEDestinationPage/CEDestinationPage.tsx b/web/src/pages/CEDestinationPage/CEDestinationPage.tsx index dd334fb953..8579c95b0a 100644 --- a/web/src/pages/CEDestinationPage/CEDestinationPage.tsx +++ b/web/src/pages/CEDestinationPage/CEDestinationPage.tsx @@ -80,7 +80,6 @@ export const CEDestinationPage = ({ destination }: Props) => { const { mutateAsync: addDestination } = useMutation({ mutationFn: api.acl.destination.addDestination, onSuccess: () => { - Snackbar.success('Destination added'); Snackbar.default('Destinations added to Pending tab and awaiting deployment.'); }, onError: (e) => { @@ -95,7 +94,6 @@ export const CEDestinationPage = ({ destination }: Props) => { const { mutateAsync: editDestination } = useMutation({ mutationFn: api.acl.destination.editDestination, onSuccess: () => { - Snackbar.success('Destination modified'); Snackbar.default('Destinations added to Pending tab and awaiting deployment.'); }, onError: (e) => { diff --git a/web/src/pages/CERulePage/CERulePage.tsx b/web/src/pages/CERulePage/CERulePage.tsx index b8bf65ab05..1685ad6160 100644 --- a/web/src/pages/CERulePage/CERulePage.tsx +++ b/web/src/pages/CERulePage/CERulePage.tsx @@ -151,7 +151,6 @@ const Content = ({ rule: initialRule }: Props) => { invalidate: ['acl'], }, onSuccess: () => { - Snackbar.success('Rule added'); Snackbar.default('Rules added to Pending tab and awaiting deployment.'); router.history.back(); }, @@ -163,7 +162,6 @@ const Content = ({ rule: initialRule }: Props) => { invalidate: ['acl'], }, onSuccess: () => { - Snackbar.success('Rule changed'); Snackbar.default('Rules added to Pending tab and awaiting deployment.'); router.history.back(); }, From d916897fb0d70af0d10ccd081d70d2a9382a1594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 08:23:09 +0100 Subject: [PATCH 09/19] formatting --- web/src/pages/CEAliasPage/CEAliasPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/CEAliasPage/CEAliasPage.tsx b/web/src/pages/CEAliasPage/CEAliasPage.tsx index 2a69879de3..7ea1e12cda 100644 --- a/web/src/pages/CEAliasPage/CEAliasPage.tsx +++ b/web/src/pages/CEAliasPage/CEAliasPage.tsx @@ -17,10 +17,10 @@ import { Controls } from '../../shared/components/Controls/Controls'; import { DescriptionBlock } from '../../shared/components/DescriptionBlock/DescriptionBlock'; import { EditPage } from '../../shared/components/EditPage/EditPage'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; -import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { TooltipContent } from '../../shared/defguard-ui/providers/tooltip/TooltipContent'; import { TooltipProvider } from '../../shared/defguard-ui/providers/tooltip/TooltipContext'; import { TooltipTrigger } from '../../shared/defguard-ui/providers/tooltip/TooltipTrigger'; From 8f0a8e5b9d81ac270a410af1731a7db1059a3a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 08:24:50 +0100 Subject: [PATCH 10/19] add dedicated endpoint to fetch current openID provider --- .../enterprise/handlers/openid_providers.rs | 39 +++++++++++++++++++ crates/defguard_core/src/lib.rs | 6 ++- web/src/shared/api/api.ts | 2 +- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index c80ef757e8..4a87e48b7a 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -372,6 +372,45 @@ pub(crate) async fn list_openid_providers( Ok(ApiResponse::json(providers, StatusCode::OK)) } +/// Get current OpenID provider. +/// +/// # Returns +/// - HTTP Status "OK" on success. +#[utoipa::path( + get, + path = "/api/v1/openid/provider/current", + tag = "OpenID", + responses( + (status = OK, description = "Get current OpenID provider"), + ), + params( + ("name" = String, Path, description = "The name of a provider",) + ) +)] +pub(crate) async fn get_current_openid_provider( + _admin: AdminRole, + State(appstate): State, +) -> ApiResult { + let settings = Settings::get_current_settings(); + let settings_json = json!({"create_account": settings.openid_create_account, + "username_handling": settings.openid_username_handling}); + match OpenIdProvider::get_current(&appstate.pool).await? { + Some(mut provider) => { + // Get rid of it, it should stay on the backend only. + provider.google_service_account_key = None; + provider.okta_private_jwk = None; + Ok(ApiResponse::new( + json!({"provider": provider, "settings": settings_json}), + StatusCode::OK, + )) + } + None => Ok(ApiResponse::new( + json!({"provider": null, "settings": settings_json}), + StatusCode::NO_CONTENT, + )), + } +} + pub(crate) async fn test_dirsync_connection( _license: LicenseInfo, _admin: AdminRole, diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index bc6ae24ca6..083e88e9ec 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -99,8 +99,9 @@ use crate::{ enterprise_settings::{get_enterprise_settings, patch_enterprise_settings}, openid_login::{auth_callback, get_auth_info}, openid_providers::{ - add_openid_provider, delete_openid_provider, get_openid_provider, - list_openid_providers, modify_openid_provider, test_dirsync_connection, + add_openid_provider, delete_openid_provider, get_current_openid_provider, + get_openid_provider, list_openid_providers, modify_openid_provider, + test_dirsync_connection, }, }, snat::handlers::{ @@ -394,6 +395,7 @@ pub fn build_webapp( .put(modify_openid_provider) .delete(delete_openid_provider), ) + .route("/provider/current", get(get_current_openid_provider)) .route("/callback", post(auth_callback)) .route("/auth_info", get(get_auth_info)), ); diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index b0aec85709..734756912c 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -424,7 +424,7 @@ const api = { getLdapConnectionStatus: () => client.get(`/ldap/test`), }, openIdProvider: { - getOpenIdProvider: () => client.get('/openid/provider'), + getOpenIdProvider: () => client.get('/openid/provider/current'), addOpenIdProvider: (data: AddOpenIdProvider) => client.post('/openid/provider', data), deleteOpenIdProvider: (name: string) => client.delete(`/openid/provider/${name}`), editOpenIdProvider: (data: AddOpenIdProvider) => From 2d70ea53ec295a470080ca274c26dc40b7eef18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 08:31:24 +0100 Subject: [PATCH 11/19] update ui submodule --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index c99a4b62dc..9324251835 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit c99a4b62dca79d866666969d8ac5f2471be8219d +Subproject commit 9324251835b78599d3c1f8313b58d81485108dd4 From 6bdaa1cad948bdbfae25790ab0e14e6729b7c244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 08:31:57 +0100 Subject: [PATCH 12/19] formatting --- web/src/shared/api/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 734756912c..02d56840b8 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -424,7 +424,8 @@ const api = { getLdapConnectionStatus: () => client.get(`/ldap/test`), }, openIdProvider: { - getOpenIdProvider: () => client.get('/openid/provider/current'), + getOpenIdProvider: () => + client.get('/openid/provider/current'), addOpenIdProvider: (data: AddOpenIdProvider) => client.post('/openid/provider', data), deleteOpenIdProvider: (name: string) => client.delete(`/openid/provider/${name}`), editOpenIdProvider: (data: AddOpenIdProvider) => From d04da628bc149e8244cd05b3de7579f47cb73b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 08:45:28 +0100 Subject: [PATCH 13/19] fix external openid form redirect --- .../AddExternalOpenIdWizardPage/useAddExternalOpenIdStore.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/AddExternalOpenIdWizardPage/useAddExternalOpenIdStore.tsx b/web/src/pages/AddExternalOpenIdWizardPage/useAddExternalOpenIdStore.tsx index 8492d444e9..0a13af7ab0 100644 --- a/web/src/pages/AddExternalOpenIdWizardPage/useAddExternalOpenIdStore.tsx +++ b/web/src/pages/AddExternalOpenIdWizardPage/useAddExternalOpenIdStore.tsx @@ -118,7 +118,7 @@ export const useAddExternalOpenIdStore = create()( }); }, initialize: (provider) => { - const initialProviderState = addExternalOpenIdStoreDefaults.providerState; + const initialProviderState = { ...addExternalOpenIdStoreDefaults.providerState }; initialProviderState.name = provider; initialProviderState.kind = provider; if (provider !== OpenIdProviderKind.Custom) { @@ -137,6 +137,7 @@ export const useAddExternalOpenIdStore = create()( break; } set({ + provider, activeStep: 'client-settings', providerState: initialProviderState, }); From ebbaf6bf4a158de498567e2b982ab92eedc5bce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 08:55:06 +0100 Subject: [PATCH 14/19] remove duplicate --- web/src/pages/CEAliasPage/CEAliasPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/pages/CEAliasPage/CEAliasPage.tsx b/web/src/pages/CEAliasPage/CEAliasPage.tsx index 7ea1e12cda..7202bcd0ff 100644 --- a/web/src/pages/CEAliasPage/CEAliasPage.tsx +++ b/web/src/pages/CEAliasPage/CEAliasPage.tsx @@ -139,11 +139,10 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => { }; if (isPresent(alias)) { await editAlias({ ...toSend, id: alias.id }); - Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); } else { await addAlias(toSend); - Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); } + Snackbar.default('Aliases added to Pending tab and awaiting deployment.'); router.history.back(); }, }); From 900bfb280c0360a59dd63add3d3aa82fe2b5650c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 09:31:35 +0100 Subject: [PATCH 15/19] fix url parsing --- .../form/EditMicrosoftProviderForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx index 7e71aee422..f418e28a79 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx @@ -47,7 +47,8 @@ export const EditMicrosoftProviderForm = ({ onSubmit, }: EditProviderFormProps) => { const defaultValues = useMemo((): FormFields => { - const tenantId = provider.base_url.split('/')[provider.base_url.length - 2]; + const urlParts = provider.base_url.split('/'); + const tenantId = urlParts[urlParts.length - 2] ?? ''; return { client_id: provider.client_id, client_secret: provider.client_secret, From 13e30980128493ed42de323d9aab0a75d201d6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 10:53:55 +0100 Subject: [PATCH 16/19] fix groups handling --- .../forms/MicrosoftProviderForm.tsx | 22 +++++++++++++++---- .../forms/schemas.ts | 2 +- .../form/EditMicrosoftProviderForm.tsx | 16 ++++++++++++-- web/src/shared/api/types.ts | 2 +- web/src/shared/utils/csv.ts | 11 ++++++++++ 5 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 web/src/shared/utils/csv.ts diff --git a/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx b/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx index 2b3fb8c9bb..81643a8059 100644 --- a/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx +++ b/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx @@ -6,6 +6,7 @@ import { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/ import { ThemeSpacing } from '../../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../../shared/form'; import { formChangeLogic } from '../../../../../shared/formLogic'; +import { joinCsv, splitCsv } from '../../../../../shared/utils/csv'; import { directorySyncBehaviorOptions, directorySyncTargetOptions, @@ -29,7 +30,7 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => { const defaultValues = useMemo( (): FormFields => ({ directory_sync_admin_behavior: providerState.directory_sync_admin_behavior, - directory_sync_group_match: providerState.directory_sync_group_match ?? null, + directory_sync_group_match: joinCsv(providerState.directory_sync_group_match), directory_sync_interval: providerState.directory_sync_interval, directory_sync_target: providerState.directory_sync_target, directory_sync_user_behavior: providerState.directory_sync_user_behavior, @@ -46,7 +47,10 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => { onChange: microsoftProviderSyncSchema, }, onSubmit: async ({ value }) => { - await onSubmit(value); + await onSubmit({ + ...value, + directory_sync_group_match: splitCsv(value.directory_sync_group_match ?? ''), + }); }, }); @@ -113,10 +117,20 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => { { - back(form.state.values); + back({ + ...form.state.values, + directory_sync_group_match: splitCsv( + form.state.values.directory_sync_group_match ?? '', + ), + }); }} onNext={() => { - mutate(form.state.values); + mutate({ + ...form.state.values, + directory_sync_group_match: splitCsv( + form.state.values.directory_sync_group_match ?? '', + ), + }); }} /> diff --git a/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/schemas.ts b/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/schemas.ts index f33ee3ea63..cbd825a805 100644 --- a/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/schemas.ts +++ b/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/schemas.ts @@ -33,7 +33,7 @@ export const googleProviderSyncSchema = baseExternalProviderSyncSchema.extend({ export const microsoftProviderSyncSchema = baseExternalProviderSyncSchema.extend({ prefetch_users: z.boolean(), - directory_sync_group_match: z.string().trim().nullable(), + directory_sync_group_match: z.string().trim(), }); export const oktaProviderSyncSchema = baseExternalProviderSyncSchema.extend({ diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx index f418e28a79..a0c901dd68 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx @@ -9,6 +9,7 @@ import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/Siz import { ThemeSpacing } from '../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../shared/form'; import { formChangeLogic } from '../../../../shared/formLogic'; +import { joinCsv, splitCsv } from '../../../../shared/utils/csv'; import { directorySyncBehaviorOptions, directorySyncTargetOptions, @@ -28,6 +29,7 @@ const basicSchema = z .string(m.form_error_required()) .trim() .min(1, m.form_error_required()), + directory_sync_group_match: z.string().trim().optional(), }) .extend(omit(baseExternalProviderConfigSchema.shape, ['base_url'])); @@ -60,7 +62,13 @@ export const EditMicrosoftProviderForm = ({ directory_sync_target: provider.directory_sync_target, directory_sync_user_behavior: provider.directory_sync_user_behavior, directory_sync_enabled: provider.directory_sync_enabled, - directory_sync_group_match: provider.directory_sync_group_match ?? '', + directory_sync_group_match: joinCsv( + Array.isArray(provider.directory_sync_group_match) + ? provider.directory_sync_group_match + : provider.directory_sync_group_match + ? [provider.directory_sync_group_match] + : null, + ), microsoftTenantId: tenantId, }; }, [provider]); @@ -74,7 +82,11 @@ export const EditMicrosoftProviderForm = ({ }, onSubmit: async ({ value }) => { const base_url = formatMicrosoftBaseUrl(value.microsoftTenantId); - await onSubmit({ ...value, base_url }); + await onSubmit({ + ...value, + base_url, + directory_sync_group_match: splitCsv(value.directory_sync_group_match ?? ''), + }); }, }); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 4cd865a130..f5a5904385 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -876,7 +876,7 @@ export interface OpenIdProvider { directory_sync_target: DirectorySyncTargetValue; okta_private_jwk?: string | null; okta_dirsync_client_id?: string | null; - directory_sync_group_match?: string | null; + directory_sync_group_match?: string[] | null; jumpcloud_api_key?: string | null; prefetch_users: boolean; } diff --git a/web/src/shared/utils/csv.ts b/web/src/shared/utils/csv.ts new file mode 100644 index 0000000000..e1af6c4753 --- /dev/null +++ b/web/src/shared/utils/csv.ts @@ -0,0 +1,11 @@ +export const joinCsv = (values?: string[] | null): string => { + if (!values || values.length === 0) return ''; + return values.join(', '); +}; + +export const splitCsv = (value: string): string[] => { + return value + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0); +}; From ea899a1e2bc51215ed0bc86af154da11e84c533a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 10:54:02 +0100 Subject: [PATCH 17/19] add missing initializer --- .../form/EditMicrosoftProviderForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx index a0c901dd68..19cf246f2e 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx @@ -62,6 +62,7 @@ export const EditMicrosoftProviderForm = ({ directory_sync_target: provider.directory_sync_target, directory_sync_user_behavior: provider.directory_sync_user_behavior, directory_sync_enabled: provider.directory_sync_enabled, + prefetch_users: provider.prefetch_users ?? false, directory_sync_group_match: joinCsv( Array.isArray(provider.directory_sync_group_match) ? provider.directory_sync_group_match From 95fd1e308707600bf2aba032c1371ee09bbde76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 11:00:05 +0100 Subject: [PATCH 18/19] fix typing for API requests --- .../forms/MicrosoftProviderForm.tsx | 10 ++++------ .../SettingsEditOpenIdProviderPage.tsx | 7 ++++++- .../form/EditMicrosoftProviderForm.tsx | 4 ++-- web/src/shared/api/types.ts | 8 +++++++- web/src/shared/utils/csv.ts | 6 ++++-- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx b/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx index 81643a8059..aea2f0a601 100644 --- a/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx +++ b/web/src/pages/AddExternalOpenIdWizardPage/steps/AddExternalOpenIdDirectoryStep/forms/MicrosoftProviderForm.tsx @@ -6,7 +6,7 @@ import { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/ import { ThemeSpacing } from '../../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../../shared/form'; import { formChangeLogic } from '../../../../../shared/formLogic'; -import { joinCsv, splitCsv } from '../../../../../shared/utils/csv'; +import { joinCsv } from '../../../../../shared/utils/csv'; import { directorySyncBehaviorOptions, directorySyncTargetOptions, @@ -49,7 +49,7 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => { onSubmit: async ({ value }) => { await onSubmit({ ...value, - directory_sync_group_match: splitCsv(value.directory_sync_group_match ?? ''), + directory_sync_group_match: value.directory_sync_group_match ?? '', }); }, }); @@ -119,17 +119,15 @@ export const MicrosoftProviderForm = ({ onSubmit }: ProviderFormProps) => { onBack={() => { back({ ...form.state.values, - directory_sync_group_match: splitCsv( + directory_sync_group_match: form.state.values.directory_sync_group_match ?? '', - ), }); }} onNext={() => { mutate({ ...form.state.values, - directory_sync_group_match: splitCsv( + directory_sync_group_match: form.state.values.directory_sync_group_match ?? '', - ), }); }} /> diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/SettingsEditOpenIdProviderPage.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/SettingsEditOpenIdProviderPage.tsx index 12fd111fb0..c953189cac 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/SettingsEditOpenIdProviderPage.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/SettingsEditOpenIdProviderPage.tsx @@ -6,6 +6,7 @@ import { type AddOpenIdProvider, OpenIdProviderKind } from '../../../shared/api/ import { EditPage } from '../../../shared/components/EditPage/EditPage'; import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { getExternalProviderQueryOptions } from '../../../shared/query'; +import { joinCsv } from '../../../shared/utils/csv'; import { EditCustomProviderForm } from './form/EditCustomProviderForm'; import { EditGoogleProviderForm } from './form/EditGoogleProviderForm'; import { EditJumpCloudProviderForm } from './form/EditJumpCloudProviderForm'; @@ -54,7 +55,11 @@ export const SettingsEditOpenIdProviderPage = () => { const handleSubmit = useCallback( async (values: Partial) => { if (isPresent(formData)) { - await mutateAsync({ ...formData, ...values }); + const normalizedFormData = { + ...formData, + directory_sync_group_match: joinCsv(formData.directory_sync_group_match), + }; + await mutateAsync({ ...normalizedFormData, ...values }); } }, [formData, mutateAsync], diff --git a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx index 19cf246f2e..402e28919d 100644 --- a/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx +++ b/web/src/pages/settings/SettingsEditOpenIdProviderPage/form/EditMicrosoftProviderForm.tsx @@ -9,7 +9,7 @@ import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/Siz import { ThemeSpacing } from '../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../shared/form'; import { formChangeLogic } from '../../../../shared/formLogic'; -import { joinCsv, splitCsv } from '../../../../shared/utils/csv'; +import { joinCsv } from '../../../../shared/utils/csv'; import { directorySyncBehaviorOptions, directorySyncTargetOptions, @@ -86,7 +86,7 @@ export const EditMicrosoftProviderForm = ({ await onSubmit({ ...value, base_url, - directory_sync_group_match: splitCsv(value.directory_sync_group_match ?? ''), + directory_sync_group_match: value.directory_sync_group_match ?? '', }); }, }); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index f5a5904385..d5b788419f 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -888,7 +888,13 @@ export interface OpenIdProviders { export type OpenIdProvidersResponse = OpenIdProviders | undefined; -export type AddOpenIdProvider = Omit & OpenIdProviderSettings; +export type AddOpenIdProvider = Omit< + OpenIdProvider, + 'id' | 'directory_sync_group_match' +> & + OpenIdProviderSettings & { + directory_sync_group_match?: string | null; + }; export interface TestDirectorySyncResponse { success: boolean; diff --git a/web/src/shared/utils/csv.ts b/web/src/shared/utils/csv.ts index e1af6c4753..f36d27f7d9 100644 --- a/web/src/shared/utils/csv.ts +++ b/web/src/shared/utils/csv.ts @@ -1,5 +1,7 @@ -export const joinCsv = (values?: string[] | null): string => { - if (!values || values.length === 0) return ''; +export const joinCsv = (values?: string[] | string | null): string => { + if (!values) return ''; + if (typeof values === 'string') return values; + if (values.length === 0) return ''; return values.join(', '); }; From 4975db95c819aa6255e879bcef8adf680c1cb9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 3 Mar 2026 11:39:06 +0100 Subject: [PATCH 19/19] update remaining fields in provider update endpoint --- .../enterprise/handlers/openid_providers.rs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 4a87e48b7a..0d215509aa 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -328,11 +328,83 @@ pub(crate) async fn modify_openid_provider( let mut transaction = appstate.pool.begin().await?; let provider = OpenIdProvider::find_by_name(&mut *transaction, &provider_data.name).await?; if let Some(mut provider) = provider { + let private_key = match &provider_data.google_service_account_key { + Some(key) => { + if RsaPrivateKey::from_pkcs8_pem(key).is_ok() { + debug!( + "User {} provided a valid RSA private key for provider's directory sync. Using it.", + session.user.username + ); + provider_data.google_service_account_key.clone() + } else { + debug!( + "User {} did not provide a valid RSA private key for provider's directory sync or the key did not change. Using the existing key", + session.user.username + ); + provider.google_service_account_key.clone() + } + } + None => provider.google_service_account_key.clone(), + }; + + let okta_private_jwk = match &provider_data.okta_private_jwk { + Some(key) => { + if serde_json::from_str::(key).is_ok() { + debug!( + "User {} provided a valid JWK private key for provider's Okta directory sync. Using it.", + session.user.username + ); + provider_data.okta_private_jwk.clone() + } else { + debug!( + "User {} did not provide a valid JWK private key for provider's Okta directory sync or the key did not change. Using the existing key.", + session.user.username + ); + provider.okta_private_jwk.clone() + } + } + None => provider.okta_private_jwk.clone(), + }; + + let mut settings = Settings::get_current_settings(); + settings.openid_create_account = provider_data.create_account; + settings.openid_username_handling = provider_data.username_handling; + update_current_settings(&appstate.pool, settings).await?; + + let group_match = if let Some(group_match) = provider_data.directory_sync_group_match { + if group_match.is_empty() { + Vec::new() + } else { + group_match + .split(',') + .map(|s| s.trim().to_string()) + .collect() + } + } else { + Vec::new() + }; + provider.base_url = provider_data.base_url; provider.kind = provider_data.kind; provider.client_id = provider_data.client_id; provider.client_secret = provider_data.client_secret; + provider.display_name = provider_data.display_name; + provider.google_service_account_key = private_key; + provider.google_service_account_email = provider_data.google_service_account_email; + provider.admin_email = provider_data.admin_email; + provider.directory_sync_enabled = provider_data.directory_sync_enabled; + provider.directory_sync_interval = provider_data.directory_sync_interval; + provider.directory_sync_user_behavior = provider_data.directory_sync_user_behavior.into(); + provider.directory_sync_admin_behavior = provider_data.directory_sync_admin_behavior.into(); + provider.directory_sync_target = provider_data.directory_sync_target.into(); + provider.okta_private_jwk = okta_private_jwk; + provider.okta_dirsync_client_id = provider_data.okta_dirsync_client_id; + provider.directory_sync_group_match = group_match; + provider.jumpcloud_api_key = provider_data.jumpcloud_api_key; + provider.prefetch_users = provider_data.prefetch_users; provider.save(&mut *transaction).await?; + transaction.commit().await?; + info!( "User {} modified OpenID client {}", session.user.username, provider.name