From 41b3d70420bc2105f44517604db2f93d0b85579d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:56:20 +0800 Subject: [PATCH 1/8] Fix Alternate Email Addresses validator blocking edit user submission This was blocking submission of the edit user page, also its a comma separated list so corrected validator and backend apis. Tested with new users with 0,1 and 2 emails set as alternatives in both the user creator and the user editor pages. --- .../CippAddTenantGroupDrawer.jsx | 3 +-- .../CippFormPages/CippAddEditUser.jsx | 21 +++++++++++++++---- .../identity/administration/users/add.jsx | 3 ++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/CippComponents/CippAddTenantGroupDrawer.jsx b/src/components/CippComponents/CippAddTenantGroupDrawer.jsx index 75804c15f89c..a82e0fd3f791 100644 --- a/src/components/CippComponents/CippAddTenantGroupDrawer.jsx +++ b/src/components/CippComponents/CippAddTenantGroupDrawer.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Button, Box } from "@mui/material"; import { useForm, useFormState } from "react-hook-form"; import { GroupAdd } from "@mui/icons-material"; @@ -6,7 +6,6 @@ import { CippOffCanvas } from "./CippOffCanvas"; import { CippApiResults } from "./CippApiResults"; import { ApiPostCall } from "../../api/ApiCall"; import CippAddEditTenantGroups from "./CippAddEditTenantGroups"; -import { getCippValidator } from "../../utils/get-cipp-validator"; export const CippAddTenantGroupDrawer = ({ buttonText = "Add Tenant Group", diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index edcc5e7cb73c..b479cacd197a 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -112,6 +112,21 @@ const CippAddEditUser = (props) => { return username.toLowerCase(); }; + const validateOtherMails = (value) => { + if (!value || (Array.isArray(value) && value.length === 0)) { + return true; + } + + const emailList = (Array.isArray(value) ? value.join(",") : value) + .split(",") + .map((email) => email.trim()) + .filter(Boolean); + + const invalidEmail = emailList.find((email) => getCippValidator(email, "email") !== true); + + return !invalidEmail || `This is not a valid email: ${invalidEmail}`; + }; + useEffect(() => { //if watch.firstname changes, and watch.lastname changes, set displayname to firstname + lastname if (watcher.givenName && watcher.surname && formType === "add") { @@ -331,7 +346,6 @@ const CippAddEditUser = (props) => { setDisplayNameManuallySet(true); }} required={true} - validators={{ required: "Display Name is required" }} /> @@ -356,7 +370,6 @@ const CippAddEditUser = (props) => { setUsernameManuallySet(true); }} required={true} - validators={{ required: "Username is required" }} /> @@ -603,10 +616,10 @@ const CippAddEditUser = (props) => { !value || getCippValidator(value, "email") }} + validators={{ validate: validateOtherMails }} /> {userSettingsDefaults?.userAttributes diff --git a/src/pages/identity/administration/users/add.jsx b/src/pages/identity/administration/users/add.jsx index 28606efc90ba..46a8c07fac61 100644 --- a/src/pages/identity/administration/users/add.jsx +++ b/src/pages/identity/administration/users/add.jsx @@ -2,13 +2,13 @@ import { Box } from "@mui/material"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; -import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; import { useSettings } from "../../../../hooks/use-settings"; import { useEffect } from "react"; import CippAddEditUser from "../../../../components/CippFormPages/CippAddEditUser"; const Page = () => { const userSettingsDefaults = useSettings(); + const tenantDomain = useSettings().currentTenant; const formControl = useForm({ mode: "onBlur", @@ -54,6 +54,7 @@ const Page = () => { title="User" backButtonTitle="User Overview" postUrl="/api/AddUser" + relatedQueryKeys={`Users - ${tenantDomain}`} > From 234786c31ed34146ba4f5093bb0de678174cac95 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 08:53:52 +0000 Subject: [PATCH 2/8] Add Mailbox Forwarding report page Add a new Mailbox Forwarding report and menu entry. Updates layouts config to include the new report under email reports with Exchange.Mailbox.* permissions. Adds src/pages/email/reports/mailbox-forwarding/index.js which renders a CippTablePage showing forwarding-related columns, a Forwarding Only toggle, tenant-aware behavior, and a Sync action that opens a CippApiDialog to call /api/ExecCIPPDBCache. Includes a CippQueueTracker to surface queue status, disables sync for AllTenants, and shows a warning if no tenant is selected. --- src/layouts/config.js | 5 + .../email/reports/mailbox-forwarding/index.js | 124 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/pages/email/reports/mailbox-forwarding/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index 78a9cd4ed9d5..41c2ce0ed418 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -741,6 +741,11 @@ export const nativeMenuItems = [ path: "/email/reports/calendar-permissions", permissions: ["Exchange.Mailbox.*"], }, + { + title: "Mailbox Forwarding", + path: "/email/reports/mailbox-forwarding", + permissions: ["Exchange.Mailbox.*"], + }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters", diff --git a/src/pages/email/reports/mailbox-forwarding/index.js b/src/pages/email/reports/mailbox-forwarding/index.js new file mode 100644 index 000000000000..63a74e80578d --- /dev/null +++ b/src/pages/email/reports/mailbox-forwarding/index.js @@ -0,0 +1,124 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { useState } from "react"; +import { + Button, + FormControlLabel, + Switch, + Alert, + SvgIcon, + IconButton, + Tooltip, +} from "@mui/material"; +import { useSettings } from "../../../../hooks/use-settings"; +import { Stack } from "@mui/system"; +import { Sync, Info } from "@mui/icons-material"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; +import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; + +const Page = () => { + const [forwardingOnly, setForwardingOnly] = useState(true); + const currentTenant = useSettings().currentTenant; + const syncDialog = useDialog(); + const [syncQueueId, setSyncQueueId] = useState(null); + + const isAllTenants = currentTenant === "AllTenants"; + + const columns = [ + ...(isAllTenants ? ["Tenant"] : []), + "UPN", + "DisplayName", + "RecipientTypeDetails", + "ForwardingType", + "ForwardTo", + "DeliverToMailboxAndForward", + "CacheTimestamp", + ]; + + const apiData = { + ForwardingOnly: forwardingOnly, + }; + + const pageActions = [ + + + + + + + + + setForwardingOnly(e.target.checked)} + color="primary" + /> + } + label="Forwarding Only" + labelPlacement="start" + /> + , + ]; + + return ( + <> + {currentTenant && currentTenant !== "" ? ( + + ) : ( + Please select a tenant to view mailbox forwarding settings. + )} + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result.Metadata.QueueId); + } + }, + }} + /> + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From b913d3530bb7e62d6158699c0e32ad9273a93fbd Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 09:24:52 +0000 Subject: [PATCH 3/8] Add UseReportDB parameter for consistency with other reports --- src/pages/email/reports/mailbox-forwarding/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/email/reports/mailbox-forwarding/index.js b/src/pages/email/reports/mailbox-forwarding/index.js index 63a74e80578d..12587bea05e5 100644 --- a/src/pages/email/reports/mailbox-forwarding/index.js +++ b/src/pages/email/reports/mailbox-forwarding/index.js @@ -37,6 +37,7 @@ const Page = () => { ]; const apiData = { + UseReportDB: true, ForwardingOnly: forwardingOnly, }; From 5e14055087d3d0a31aae13274412084d9d747fcb Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 10:19:11 +0000 Subject: [PATCH 4/8] Add pre-saved filters for forwarding type Adds filter presets for External, Internal, and Both forwarding types. --- .../email/reports/mailbox-forwarding/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pages/email/reports/mailbox-forwarding/index.js b/src/pages/email/reports/mailbox-forwarding/index.js index 12587bea05e5..a43d9834f7f2 100644 --- a/src/pages/email/reports/mailbox-forwarding/index.js +++ b/src/pages/email/reports/mailbox-forwarding/index.js @@ -41,6 +41,24 @@ const Page = () => { ForwardingOnly: forwardingOnly, }; + const filters = [ + { + filterName: "External Forwarding", + value: [{ id: "ForwardingType", value: "External" }], + type: "column", + }, + { + filterName: "Internal Forwarding", + value: [{ id: "ForwardingType", value: "Internal" }], + type: "column", + }, + { + filterName: "Both (External & Internal)", + value: [{ id: "ForwardingType", value: "Both" }], + type: "column", + }, + ]; + const pageActions = [ { queryKey={`mailbox-forwarding-${currentTenant}-${forwardingOnly}`} apiData={apiData} simpleColumns={columns} + filters={filters} cardButton={pageActions} offCanvas={null} /> From 347aa53613066e1addae22b40c187df1cd6361dc Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 10:39:29 +0000 Subject: [PATCH 5/8] Remove ForwardingOnly toggle - always show only forwarding mailboxes A mailbox forwarding report should only contain mailboxes with forwarding. --- .../email/reports/mailbox-forwarding/index.js | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/pages/email/reports/mailbox-forwarding/index.js b/src/pages/email/reports/mailbox-forwarding/index.js index a43d9834f7f2..56ed6971e054 100644 --- a/src/pages/email/reports/mailbox-forwarding/index.js +++ b/src/pages/email/reports/mailbox-forwarding/index.js @@ -1,15 +1,7 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import { useState } from "react"; -import { - Button, - FormControlLabel, - Switch, - Alert, - SvgIcon, - IconButton, - Tooltip, -} from "@mui/material"; +import { Button, Alert, SvgIcon, IconButton, Tooltip } from "@mui/material"; import { useSettings } from "../../../../hooks/use-settings"; import { Stack } from "@mui/system"; import { Sync, Info } from "@mui/icons-material"; @@ -18,7 +10,6 @@ import { CippApiDialog } from "../../../../components/CippComponents/CippApiDial import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; const Page = () => { - const [forwardingOnly, setForwardingOnly] = useState(true); const currentTenant = useSettings().currentTenant; const syncDialog = useDialog(); const [syncQueueId, setSyncQueueId] = useState(null); @@ -38,7 +29,6 @@ const Page = () => { const apiData = { UseReportDB: true, - ForwardingOnly: forwardingOnly, }; const filters = [ @@ -63,7 +53,7 @@ const Page = () => { @@ -83,18 +73,6 @@ const Page = () => { > Sync - setForwardingOnly(e.target.checked)} - color="primary" - /> - } - label="Forwarding Only" - labelPlacement="start" - /> , ]; @@ -102,10 +80,9 @@ const Page = () => { <> {currentTenant && currentTenant !== "" ? ( { type: "GET", url: "/api/ExecCIPPDBCache", confirmText: `Run mailbox cache sync for ${currentTenant}? This will update mailbox data including forwarding settings.`, - relatedQueryKeys: [`mailbox-forwarding-${currentTenant}-${forwardingOnly}`], + relatedQueryKeys: [`mailbox-forwarding-${currentTenant}`], data: { Name: "Mailboxes", Types: "", From d05df7653ec42e0a3608889546cd4d756b3b3376 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 13:12:15 +0000 Subject: [PATCH 6/8] Remove Both filter - only External and Internal --- src/pages/email/reports/mailbox-forwarding/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/email/reports/mailbox-forwarding/index.js b/src/pages/email/reports/mailbox-forwarding/index.js index 56ed6971e054..008637df1a97 100644 --- a/src/pages/email/reports/mailbox-forwarding/index.js +++ b/src/pages/email/reports/mailbox-forwarding/index.js @@ -42,11 +42,6 @@ const Page = () => { value: [{ id: "ForwardingType", value: "Internal" }], type: "column", }, - { - filterName: "Both (External & Internal)", - value: [{ id: "ForwardingType", value: "Both" }], - type: "column", - }, ]; const pageActions = [ From 87dd23b5b1617c80f0dc6daea90c6e66e6f8ec5e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Mar 2026 11:31:02 -0400 Subject: [PATCH 7/8] chore: bump version to 10.2.1 --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 06670325843f..05a3468c52e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.2.0", + "version": "10.2.1", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 7e488263a60c..6fafb4fbb418 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.2.0" + "version": "10.2.1" } \ No newline at end of file From 15f3256e1ca6057075091e60932959101f023e77 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:22:44 +0100 Subject: [PATCH 8/8] fix: readd accidentally removed tenant selector shortcut --- src/layouts/top-nav.js | 150 +++++++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 44 deletions(-) diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index e53973075533..370fd0f7ddbd 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -64,6 +64,7 @@ export const TopNav = (props) => { const [flashLock, setFlashLock] = useState(false); const itemRefs = useRef({}); const touchDragRef = useRef({ startIdx: null, overIdx: null }); + const tenantSelectorRef = useRef(null); const handleBookmarkClick = (event) => { setAnchorEl(event.currentTarget); @@ -148,7 +149,10 @@ export const TopNav = (props) => { if (index <= 0 || animatingPair) return; const el1 = itemRefs.current[index]; const el2 = itemRefs.current[index - 1]; - if (!el1 || !el2) { moveBookmarkUp(index); return; } + if (!el1 || !el2) { + moveBookmarkUp(index); + return; + } const distance = el1.getBoundingClientRect().top - el2.getBoundingClientRect().top; setAnimatingPair({ idx1: index, idx2: index - 1, offset1: -distance, offset2: distance }); setTimeout(() => { @@ -162,7 +166,10 @@ export const TopNav = (props) => { if (index >= bookmarks.length - 1 || animatingPair) return; const el1 = itemRefs.current[index]; const el2 = itemRefs.current[index + 1]; - if (!el1 || !el2) { moveBookmarkDown(index); return; } + if (!el1 || !el2) { + moveBookmarkDown(index); + return; + } const distance = el2.getBoundingClientRect().top - el1.getBoundingClientRect().top; setAnimatingPair({ idx1: index, idx2: index + 1, offset1: distance, offset2: -distance }); setTimeout(() => { @@ -188,9 +195,16 @@ export const TopNav = (props) => { const popoverOpen = Boolean(anchorEl); const popoverId = popoverOpen ? "bookmark-popover" : undefined; + const openSearch = useCallback(() => { + searchDialog.handleOpen(); + }, [searchDialog.handleOpen]); + useEffect(() => { const handleKeyDown = (event) => { - if ((event.metaKey || event.ctrlKey) && event.key === "k") { + if ((event.metaKey || event.ctrlKey) && event.altKey && event.key === "k") { + event.preventDefault(); + tenantSelectorRef.current?.focus(); + } else if ((event.metaKey || event.ctrlKey) && event.key === "k") { event.preventDefault(); openSearch(); } @@ -199,11 +213,7 @@ export const TopNav = (props) => { return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, []); - - const openSearch = () => { - searchDialog.handleOpen(); - }; + }, [openSearch]); return ( { > - {!mdDown && } + {!mdDown && ( + + )} {mdDown && ( @@ -331,7 +343,13 @@ export const TopNav = (props) => { }, }), }} - title={sortOrder === "custom" ? "Custom order" : sortOrder === "asc" ? "A > Z" : "Z > A"} + title={ + sortOrder === "custom" + ? "Custom order" + : sortOrder === "asc" + ? "A > Z" + : "Z > A" + } > {sortOrder === "custom" && } {sortOrder === "asc" && } @@ -341,7 +359,11 @@ export const TopNav = (props) => { variant="body2" sx={{ ml: 0.5, color: "text.secondary", fontSize: 12 }} > - {sortOrder === "custom" ? "Custom order" : sortOrder === "asc" ? "A > Z" : "Z > A"} + {sortOrder === "custom" + ? "Custom order" + : sortOrder === "asc" + ? "A > Z" + : "Z > A"} @@ -355,21 +377,35 @@ export const TopNav = (props) => { displayBookmarks.map((bookmark, idx) => ( { itemRefs.current[idx] = el; }} + ref={(el) => { + itemRefs.current[idx] = el; + }} data-bookmark-index={idx} draggable={reorderMode === "drag" && sortOrder === "custom" && !locked} - {...(reorderMode === "drag" ? { - onDragStart: (e) => { - if (locked) { e.preventDefault(); triggerLockFlash(); return; } - if (sortOrder !== "custom") { e.preventDefault(); triggerSortFlash(); return; } - handleDragStart(idx); - }, - onDragEnd: handleDragEnd, - ...(sortOrder === "custom" && !locked ? { - onDragOver: (e) => handleDragOver(e, idx), - onDrop: (e) => handleDrop(e, idx), - } : {}), - } : {})} + {...(reorderMode === "drag" + ? { + onDragStart: (e) => { + if (locked) { + e.preventDefault(); + triggerLockFlash(); + return; + } + if (sortOrder !== "custom") { + e.preventDefault(); + triggerSortFlash(); + return; + } + handleDragStart(idx); + }, + onDragEnd: handleDragEnd, + ...(sortOrder === "custom" && !locked + ? { + onDragOver: (e) => handleDragOver(e, idx), + onDrop: (e) => handleDrop(e, idx), + } + : {}), + } + : {})} sx={{ color: "inherit", display: "flex", @@ -377,25 +413,34 @@ export const TopNav = (props) => { "&:hover .bookmark-controls": { opacity: 1, }, - ...(sortOrder === "custom" && reorderMode === "drag" && dragIndex === idx && { - opacity: 0.4, - }), - ...(sortOrder === "custom" && reorderMode === "drag" && dragOverIndex === idx && dragIndex !== idx && { - borderTop: "2px solid", - borderColor: "primary.main", - }), - ...(animatingPair && (animatingPair.idx1 === idx || animatingPair.idx2 === idx) && { - transform: `translateY(${animatingPair.idx1 === idx ? animatingPair.offset1 : animatingPair.offset2}px)`, - transition: 'transform 250ms ease-in-out', - position: 'relative', - zIndex: animatingPair.idx1 === idx ? 1 : 0, - }), + ...(sortOrder === "custom" && + reorderMode === "drag" && + dragIndex === idx && { + opacity: 0.4, + }), + ...(sortOrder === "custom" && + reorderMode === "drag" && + dragOverIndex === idx && + dragIndex !== idx && { + borderTop: "2px solid", + borderColor: "primary.main", + }), + ...(animatingPair && + (animatingPair.idx1 === idx || animatingPair.idx2 === idx) && { + transform: `translateY(${animatingPair.idx1 === idx ? animatingPair.offset1 : animatingPair.offset2}px)`, + transition: "transform 250ms ease-in-out", + position: "relative", + zIndex: animatingPair.idx1 === idx ? 1 : 0, + }), }} > {reorderMode === "drag" && !locked && ( { - if (sortOrder !== "custom") { triggerSortFlash(); return; } + if (sortOrder !== "custom") { + triggerSortFlash(); + return; + } touchDragRef.current.startIdx = idx; setDragIndex(idx); }} @@ -409,7 +454,11 @@ export const TopNav = (props) => { const li = el?.closest("[data-bookmark-index]"); if (li) { const overIdx = parseInt(li.dataset.bookmarkIndex, 10); - if (!isNaN(overIdx) && overIdx >= 0 && overIdx < (settings.bookmarks || []).length) { + if ( + !isNaN(overIdx) && + overIdx >= 0 && + overIdx < (settings.bookmarks || []).length + ) { touchDragRef.current.overIdx = overIdx; setDragOverIndex(overIdx); } @@ -470,7 +519,10 @@ export const TopNav = (props) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } sortOrder === "custom" ? animatedMoveUp(idx) : triggerSortFlash(); }} disabled={sortOrder === "custom" && idx === 0} @@ -482,10 +534,17 @@ export const TopNav = (props) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } - sortOrder === "custom" ? animatedMoveDown(idx) : triggerSortFlash(); + if (locked) { + triggerLockFlash(); + return; + } + sortOrder === "custom" + ? animatedMoveDown(idx) + : triggerSortFlash(); }} - disabled={sortOrder === "custom" && idx === displayBookmarks.length - 1} + disabled={ + sortOrder === "custom" && idx === displayBookmarks.length - 1 + } sx={{ opacity: sortOrder !== "custom" || locked ? 0.4 : 1 }} > @@ -497,7 +556,10 @@ export const TopNav = (props) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } removeBookmark(bookmark.path); }} sx={{ ...(locked && { opacity: 0.4 }) }}