From 7b8af70d08c878a22f8bcb8b25401c2dcdcd83d9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 4 Mar 2025 21:46:29 -0500 Subject: [PATCH 01/13] fix: update layout adjust logic to check for when no tenant is selected --- src/layouts/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/index.js b/src/layouts/index.js index fa6e9812d5b6..c2f49483f74a 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -231,7 +231,7 @@ export const Layout = (props) => { }} > - {currentTenant === "AllTenants" && !allTenantsSupport ? ( + {(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? ( From 61044eb0da4aa321d7b1deb5c39f5d11963c7c45 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 5 Mar 2025 10:50:44 -0500 Subject: [PATCH 02/13] feat: add filter modes to global search --- src/components/CippTable/CippDataTable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 307106098392..0d7f77f0bcb3 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -287,6 +287,7 @@ export const CippDataTable = (props) => { ); }, + enableGlobalFilterModes: true, }); useEffect(() => { From 2f8aae04de2c5c4e1cc7cfc9e663756ecb763bf5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 5 Mar 2025 12:25:47 -0500 Subject: [PATCH 03/13] fix tenantFilter casing --- src/components/CippComponents/CippAutocomplete.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 94b71886d005..4fb8c073086c 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -99,7 +99,7 @@ export const CippAutoComplete = (props) => { setGetRequestInfo({ url: api.url, data: { - ...(!api.excludeTenantFilter ? { TenantFilter: currentTenant } : null), + ...(!api.excludeTenantFilter ? { tenantFilter: currentTenant } : null), ...api.data, }, waiting: true, From c8cc1775f1887a3dc97602ce4d47dd98d4613f42 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 5 Mar 2025 12:25:57 -0500 Subject: [PATCH 04/13] fix: sherweb link --- src/pages/tenant/reports/list-csp-licenses/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/reports/list-csp-licenses/index.jsx b/src/pages/tenant/reports/list-csp-licenses/index.jsx index 9e6264df4c74..b046b6ea275c 100644 --- a/src/pages/tenant/reports/list-csp-licenses/index.jsx +++ b/src/pages/tenant/reports/list-csp-licenses/index.jsx @@ -93,7 +93,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - From f60b06ac99c5409ee0adf10c92e2fcccb2f47783 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 5 Mar 2025 17:13:01 -0500 Subject: [PATCH 05/13] fix: backups and scheduler --- src/pages/cipp/scheduler/index.js | 11 ++-- src/pages/tenant/backup/backup-wizard/add.jsx | 13 +--- .../tenant/backup/backup-wizard/restore.jsx | 63 +++++++++++-------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 97abcc20b602..81cf3213ed02 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -82,19 +82,20 @@ const Page = () => { tenantInTitle={false} title="Scheduled Tasks" apiUrl={ - showHiddenJobs ? "/api/ListScheduledItems?ListHidden=True" : "/api/ListScheduledItems" + showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems" } queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`} simpleColumns={[ - "Name", - "Tenant", + "ExecutedTime", "TaskState", + "Tenant", + "Name", + "ScheduledTime", "Command", "Parameters", "PostExecution", "Recurrence", - "ExecutedTime", - "ScheduledTime", + "Results", ]} actions={actions} offCanvas={offCanvas} diff --git a/src/pages/tenant/backup/backup-wizard/add.jsx b/src/pages/tenant/backup/backup-wizard/add.jsx index 90d58127a11a..86ec994ca6c4 100644 --- a/src/pages/tenant/backup/backup-wizard/add.jsx +++ b/src/pages/tenant/backup/backup-wizard/add.jsx @@ -1,12 +1,12 @@ import React from "react"; import { Grid, Typography } from "@mui/material"; import { useForm } from "react-hook-form"; +import { omit } from "lodash"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; -import { te } from "date-fns/locale"; const CreateBackup = () => { const userSettingsDefaults = useSettings(); @@ -26,7 +26,6 @@ const CreateBackup = () => { antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, - CippStandards: true, }, }); @@ -45,7 +44,7 @@ const CreateBackup = () => { TenantFilter: tenantFilter, Name: `CIPP Backup - ${tenantFilter}`, Command: { value: `New-CIPPBackup` }, - Parameters: { backupType: "Scheduled", ScheduledBackupValues: { ...values } }, + Parameters: { backupType: "Scheduled", ScheduledBackupValues: { ...omit(values, ['tenantFilter']) } }, ScheduledTime: unixTime, Recurrence: { value: "1d" }, }; @@ -168,14 +167,6 @@ const CreateBackup = () => { formControl={formControl} /> - - - {/* Add an empty Grid item to fill the second column */} diff --git a/src/pages/tenant/backup/backup-wizard/restore.jsx b/src/pages/tenant/backup/backup-wizard/restore.jsx index 2a332140b139..d7d8c1f16c37 100644 --- a/src/pages/tenant/backup/backup-wizard/restore.jsx +++ b/src/pages/tenant/backup/backup-wizard/restore.jsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from "react"; -import { Alert, Grid, Typography } from "@mui/material"; +import { Alert, Divider, Grid, Typography } from "@mui/material"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import { Chip, Stack } from "@mui/material"; const RestoreBackupForm = () => { const userSettingsDefaults = useSettings(); @@ -85,7 +86,6 @@ const RestoreBackupForm = () => { antiphishing: values.antiphishing, CippWebhookAlerts: values.CippWebhookAlerts, CippScriptedAlerts: values.CippScriptedAlerts, - CippStandards: values.CippStandards, overwrite: values.overwrite, }, }, @@ -95,6 +95,7 @@ const RestoreBackupForm = () => { Email: values.email, PSA: values.psa, }, + DisallowDuplicateName: true, }; return shippedValues; }} @@ -104,7 +105,7 @@ const RestoreBackupForm = () => { Use this form to restore a backup for a tenant. Please select the tenant, backup, and restore options. - + {/* Backup Selector */} { name="backup" multiple={false} api={{ - tenantFilter: tenantFilter, url: "/api/ExecListBackup", queryKey: `BackupList-${tenantFilter}`, - labelField: (option) => `${option.RowKey}`, - valueField: "RowKey", + labelField: (option) => { + const match = option.BackupName.match(/.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/); + return match ? `${match[1]} @ ${match[2]}:${match[3]}` : option.BackupName; + }, + valueField: "BackupName", data: { Type: "Scheduled", - TenantFilter: tenantFilter, + NameOnly: true, }, }} formControl={formControl} + required={true} + validators={{ + validate: (value) => !!value || "Please select a backup", + }} /> @@ -210,12 +217,6 @@ const RestoreBackupForm = () => { name="CippScriptedAlerts" formControl={formControl} /> - {/* Overwrite Existing Entries */} @@ -262,38 +263,46 @@ const RestoreBackupForm = () => { - + + + {/* Review and Confirm */} - Review and Confirm - + Review and Confirm + Please review the selected options before submitting. Selected Tenant: - {tenantFilter} + + {tenantFilter} + Selected Backup: - + {formControl.watch("backup")?.label || "None selected"} Overwrite Existing Configuration: - {formControl.watch("overwrite") ? "Yes" : "No"} + + {formControl.watch("overwrite") ? "Yes" : "No"} + + Send Results To: - - {formControl.watch("webhook") && "Webhook "} - {formControl.watch("email") && "E-mail "} - {formControl.watch("psa") && "PSA "} - {!formControl.watch("webhook") && - !formControl.watch("email") && - !formControl.watch("psa") && - "None"} + + + {formControl.watch("webhook") && } + {formControl.watch("email") && } + {formControl.watch("psa") && } + {!formControl.watch("webhook") && + !formControl.watch("email") && + !formControl.watch("psa") && } + From 63f4356966d5df79221cd6a6be9321fb6d26ea7b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 5 Mar 2025 17:25:54 -0500 Subject: [PATCH 06/13] display post execution cleanly --- src/utils/get-cipp-formatting.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 25b1cd58c7ec..48e57d972f1a 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -219,6 +219,24 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { } } + if (cellName === "PostExecution") { + const values = data ? data?.split(",").map((item) => item.trim()) : []; + if (values.length > 0) { + return isText + ? data + : values.map((value, index) => ( + + )); + } + } + if (cellName === "ClientId" || cellName === "role") { return isText ? data : ; } From b4758f537db29e18dea28b69acadf515e568cc6b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 5 Mar 2025 18:05:19 -0500 Subject: [PATCH 07/13] add city --- src/pages/cipp/preferences.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js index 81fd6d446bbf..40ccac234133 100644 --- a/src/pages/cipp/preferences.js +++ b/src/pages/cipp/preferences.js @@ -35,6 +35,7 @@ const Page = () => { { value: "otherMails", label: "otherMails" }, { value: "showInAddressList", label: "showInAddressList" }, { value: "state", label: "state" }, + { value: "city", label: "city" }, { value: "sponsor", label: "sponsor" }, ]; From d43edb2392306e29727612df4245eeedf840c695 Mon Sep 17 00:00:00 2001 From: Esco Date: Thu, 6 Mar 2025 11:43:39 +0100 Subject: [PATCH 08/13] fix: hide duplicate sponsor field --- src/components/CippFormPages/CippAddEditUser.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 5087d40456fa..9cee72637872 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -269,7 +269,7 @@ const CippAddEditUser = (props) => { formControl={formControl} /> - {userSettingsDefaults?.userAttributes?.map((attribute, idx) => ( + {userSettingsDefaults?.userAttributes?.filter((attribute) => attribute.value !== "sponsor").map((attribute, idx) => ( Date: Thu, 6 Mar 2025 21:48:53 +0100 Subject: [PATCH 09/13] feat: add CippExchangeActions component with mailbox management actions --- .../CippComponents/CippExchangeActions.jsx | 240 ++++++++++++++++++ .../email/administration/mailboxes/index.js | 235 +---------------- .../administration/users/user/exchange.jsx | 3 + 3 files changed, 247 insertions(+), 231 deletions(-) create mode 100644 src/components/CippComponents/CippExchangeActions.jsx diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx new file mode 100644 index 000000000000..329fb3492eed --- /dev/null +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -0,0 +1,240 @@ +import { + EyeIcon, + TrashIcon, + MagnifyingGlassIcon, + PlayCircleIcon, +} from "@heroicons/react/24/outline"; +import { + Archive, + MailOutline, + Person, + Room, + Visibility, + VisibilityOff, + PhonelinkLock, + Key, + PostAdd, + Add, +} from "@mui/icons-material"; +import { useSettings } from "/src/hooks/use-settings.js"; + +export const CippExchangeActions = () => { + // const tenant = useSettings().currentTenant; + return [ + { + label: "Edit permissions", + link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", + color: "info", + icon: , + }, + { + label: "Research Compromised Account", + link: "/identity/administration/users/user/bec?userId=[ExternalDirectoryObjectId]", + color: "info", + icon: , + }, + { + label: "Send MFA Push", + type: "POST", + url: "/api/ExecSendPush", + data: { + UserEmail: "UPN", + }, + confirmText: "Are you sure you want to send an MFA request?", + icon: , + }, + { + label: "Convert to User Mailbox", + type: "POST", + url: "/api/ExecConvertMailbox", + icon: , + data: { + ID: "UPN", + MailboxType: "!Regular", + }, + confirmText: "Are you sure you want to convert this mailbox to a user mailbox?", + condition: (row) => row.recipientTypeDetails !== "UserMailbox", + }, + { + label: "Convert to Shared Mailbox", + type: "POST", + icon: , + url: "/api/ExecConvertMailbox", + data: { + ID: "UPN", + MailboxType: "!Shared", + }, + confirmText: "Are you sure you want to convert this mailbox to a shared mailbox?", + condition: (row) => row.recipientTypeDetails !== "SharedMailbox", + }, + { + label: "Convert to Room Mailbox", + type: "POST", + url: "/api/ExecConvertMailbox", + icon: , + data: { + ID: "UPN", + MailboxType: "!Room", + }, + confirmText: "Are you sure you want to convert this mailbox to a room mailbox?", + condition: (row) => row.recipientTypeDetails !== "RoomMailbox", + }, + { + //tested + label: "Enable Online Archive", + type: "POST", + icon: , + url: "/api/ExecEnableArchive", + data: { ID: "Id", username: "UPN" }, + confirmText: "Are you sure you want to enable the online archive for this user?", + multiPost: false, + condition: (row) => row.ArchiveGuid === "00000000-0000-0000-0000-000000000000", + }, + { + label: "Enable Auto-Expanding Archive", + type: "POST", + icon: , + url: "/api/ExecEnableAutoExpandingArchive", + data: { ID: "Id", username: "UPN" }, + confirmText: + "Are you sure you want to enable auto-expanding archive for this user? The archive must already be enabled.", + multiPost: false, + condition: (row) => row.ArchiveGuid !== "00000000-0000-0000-0000-000000000000", + }, + { + label: "Hide from Global Address List", + type: "POST", + url: "/api/ExecHideFromGAL", + icon: , + data: { + ID: "UPN", + HidefromGAL: true, + }, + confirmText: + "Are you sure you want to hide this mailbox from the global address list? This will not work if the user is AD Synced.", + condition: (row) => row.HiddenFromAddressListsEnabled === false, + }, + { + label: "Unhide from Global Address List", + type: "POST", + url: "/api/ExecHideFromGAL", + icon: , + data: { + ID: "UPN", + HidefromGAL: false, + }, + confirmText: + "Are you sure you want to unhide this mailbox from the global address list? This will not work if the user is AD Synced.", + condition: (row) => row.HiddenFromAddressListsEnabled === true, + }, + { + label: "Start Managed Folder Assistant", + type: "POST", + url: "/api/ExecStartManagedFolderAssistant", + icon: , + data: { + ID: "ExchangeGuid", + UserPrincipalName: "UPN", + }, + confirmText: "Are you sure you want to start the managed folder assistant for this user?", + }, + { + label: "Delete Mailbox", + type: "POST", + icon: , + url: "/api/RemoveUser", + data: { ID: "UPN" }, + confirmText: "Are you sure you want to delete this mailbox?", + multiPost: false, + }, + { + label: "Copy Sent Items to Shared Mailbox", + type: "POST", + url: "/api/ExecCopyForSent", + data: { ID: "UPN" }, + confirmText: "Are you sure you want to enable Copy Sent Items to Shared Mailbox?", + icon: , + condition: (row) => + row.MessageCopyForSentAsEnabled === false && row.recipientTypeDetails === "SharedMailbox", + }, + { + label: "Disable Copy Sent Items to Shared Mailbox", + type: "POST", + url: "/api/ExecCopyForSent", + data: { ID: "UPN", MessageCopyForSentAsEnabled: false }, + confirmText: "Are you sure you want to disable Copy Sent Items to Shared Mailbox?", + icon: , + condition: (row) => + row.MessageCopyForSentAsEnabled === true && row.recipientTypeDetails === "SharedMailbox", + }, + { + label: "Set mailbox locale", + type: "POST", + url: "/api/ExecSetMailboxLocale", + data: { user: "UPN", ProhibitSendQuota: true }, + confirmText: "Enter a locale, e.g. en-US", + icon: , + fields: [ + { + label: "Locale", + name: "locale", + type: "textField", + placeholder: "e.g. en-US", + }, + ], + }, + { + label: "Set Send Quota", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { user: "UPN", ProhibitSendQuota: true }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + }, + ], + }, + { + label: "Set Send and Receive Quota", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { + user: "UPN", + ProhibitSendReceiveQuota: true, + }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + }, + ], + }, + { + label: "Set Quota Warning Level", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { user: "UPN", IssueWarningQuota: true }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + }, + ], + }, + ]; +}; + +export default CippExchangeActions; diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index c4014ca25d38..da5479b87fb4 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -2,243 +2,16 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import Link from "next/link"; import { Button } from "@mui/material"; -import { - Archive, - MailOutline, - Person, - Room, - Visibility, - VisibilityOff, - PhonelinkLock, - Key, - PostAdd, - Add, -} from "@mui/icons-material"; -import { TrashIcon, MagnifyingGlassIcon, PlayCircleIcon } from "@heroicons/react/24/outline"; +import { Add } from "@mui/icons-material"; +import CippExchangeActions from "../../../../components/CippComponents/CippExchangeActions"; const Page = () => { const pageTitle = "Mailboxes"; - // Define actions for mailboxes - const actions = [ - { - label: "Edit permissions", - link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", - color: "info", - icon: , - }, - { - label: "Research Compromised Account", - link: "/identity/administration/users/user/bec?userId=[ExternalDirectoryObjectId]", - color: "info", - icon: , - }, - { - label: "Send MFA Push", - type: "POST", - url: "/api/ExecSendPush", - data: { - UserEmail: "UPN", - }, - confirmText: "Are you sure you want to send an MFA request?", - icon: , - }, - { - label: "Convert to User Mailbox", - type: "POST", - url: "/api/ExecConvertMailbox", - icon: , - data: { - ID: "UPN", - MailboxType: "!Regular", - }, - confirmText: "Are you sure you want to convert this mailbox to a user mailbox?", - condition: (row) => row.recipientTypeDetails !== "UserMailbox", - }, - { - label: "Convert to Shared Mailbox", - type: "POST", - icon: , - url: "/api/ExecConvertMailbox", - data: { - ID: "UPN", - MailboxType: "!Shared", - }, - confirmText: "Are you sure you want to convert this mailbox to a shared mailbox?", - condition: (row) => row.recipientTypeDetails !== "SharedMailbox", - }, - { - label: "Convert to Room Mailbox", - type: "POST", - url: "/api/ExecConvertMailbox", - icon: , - data: { - ID: "UPN", - MailboxType: "!Room", - }, - confirmText: "Are you sure you want to convert this mailbox to a room mailbox?", - condition: (row) => row.recipientTypeDetails !== "RoomMailbox", - }, - { - //tested - label: "Enable Online Archive", - type: "POST", - icon: , - url: "/api/ExecEnableArchive", - data: { ID: "Id", username: "UPN" }, - confirmText: "Are you sure you want to enable the online archive for this user?", - multiPost: false, - condition: (row) => row.ArchiveGuid === "00000000-0000-0000-0000-000000000000", - }, - { - label: "Enable Auto-Expanding Archive", - type: "POST", - icon: , - url: "/api/ExecEnableAutoExpandingArchive", - data: { ID: "Id", username: "UPN" }, - confirmText: - "Are you sure you want to enable auto-expanding archive for this user? The archive must already be enabled.", - multiPost: false, - condition: (row) => row.ArchiveGuid !== "00000000-0000-0000-0000-000000000000", - }, - { - label: "Hide from Global Address List", - type: "POST", - url: "/api/ExecHideFromGAL", - icon: , - data: { - ID: "UPN", - HidefromGAL: true, - }, - confirmText: - "Are you sure you want to hide this mailbox from the global address list? This will not work if the user is AD Synced.", - condition: (row) => row.HiddenFromAddressListsEnabled === false, - }, - { - label: "Unhide from Global Address List", - type: "POST", - url: "/api/ExecHideFromGAL", - icon: , - data: { - ID: "UPN", - }, - confirmText: - "Are you sure you want to unhide this mailbox from the global address list? This will not work if the user is AD Synced.", - condition: (row) => row.HiddenFromAddressListsEnabled === true, - }, - { - label: "Start Managed Folder Assistant", - type: "POST", - url: "/api/ExecStartManagedFolderAssistant", - icon: , - data: { - ID: "ExchangeGuid", - UserPrincipalName: "UPN", - }, - confirmText: "Are you sure you want to start the managed folder assistant for this user?", - }, - { - label: "Delete Mailbox", - type: "POST", - icon: , - url: "/api/RemoveUser", - data: { ID: "UPN" }, - confirmText: "Are you sure you want to delete this mailbox?", - multiPost: false, - }, - { - label: "Copy Sent Items to Shared Mailbox", - type: "POST", - url: "/api/ExecCopyForSent", - data: { ID: "UPN" }, - confirmText: "Are you sure you want to enable Copy Sent Items to Shared Mailbox?", - icon: , - condition: (row) => - row.MessageCopyForSentAsEnabled === false && row.recipientTypeDetails === "SharedMailbox", - }, - { - label: "Disable Copy Sent Items to Shared Mailbox", - type: "POST", - url: "/api/ExecCopyForSent", - data: { ID: "UPN", MessageCopyForSentAsEnabled: false }, - confirmText: "Are you sure you want to disable Copy Sent Items to Shared Mailbox?", - icon: , - condition: (row) => - row.MessageCopyForSentAsEnabled === true && row.recipientTypeDetails === "SharedMailbox", - }, - { - label: "Set mailbox locale", - type: "POST", - url: "/api/ExecSetMailboxLocale", - data: { user: "UPN", ProhibitSendQuota: true }, - confirmText: "Enter a locale, e.g. en-US", - icon: , - fields: [ - { - label: "Locale", - name: "locale", - type: "textField", - placeholder: "e.g. en-US", - }, - ], - }, - { - label: "Set Send Quota", - type: "POST", - url: "/api/ExecSetMailboxQuota", - data: { user: "UPN", ProhibitSendQuota: true }, - confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", - icon: , - fields: [ - { - label: "Quota", - name: "quota", - type: "textField", - placeholder: "e.g. 1000MB, 10GB,1TB", - }, - ], - }, - { - label: "Set Send and Receive Quota", - type: "POST", - url: "/api/ExecSetMailboxQuota", - data: { - user: "UPN", - ProhibitSendReceiveQuota: true, - }, - confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", - icon: , - fields: [ - { - label: "Quota", - name: "quota", - type: "textField", - placeholder: "e.g. 1000MB, 10GB,1TB", - }, - ], - }, - { - label: "Set Quota Warning Level", - type: "POST", - url: "/api/ExecSetMailboxQuota", - data: { user: "UPN", IssueWarningQuota: true }, - confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", - icon: , - fields: [ - { - label: "Quota", - name: "quota", - type: "textField", - placeholder: "e.g. 1000MB, 10GB,1TB", - }, - ], - }, - ]; - // Define off-canvas details const offCanvas = { extendedInfoFields: ["displayName", "UPN", "AdditionalEmailAddresses", "recipientTypeDetails"], - actions: actions, + actions: CippExchangeActions(), }; const filterList = [ @@ -278,7 +51,7 @@ const Page = () => { { const userSettingsDefaults = useSettings(); @@ -261,6 +262,8 @@ const Page = () => { tabOptions={tabOptions} title={title} subtitle={subtitle} + actions={CippExchangeActions()} + actionsData={userRequest.data?.[0]?.MailboxActionsData} isFetching={graphUserRequest.isLoading} > From ca2c0cd2c9998b789c564638c023b3d6bcc20a98 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 6 Mar 2025 17:30:07 -0500 Subject: [PATCH 10/13] feat: bulk requests on users page --- .../administration/users/user/index.jsx | 198 ++++++++---------- 1 file changed, 93 insertions(+), 105 deletions(-) diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 0d8628fdd9d5..0e6aa451885e 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; import { AdminPanelSettings, Check, Group, Mail, Fingerprint, Launch } from "@mui/icons-material"; @@ -85,33 +85,44 @@ const Page = () => { waiting: waiting, }); - const userMemberOf = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `/users/${userId}/memberOf`, - tenantFilter: userSettingsDefaults.currentTenant, - $top: 99, - }, - queryKey: `UserMemberOf-${userId}`, + const userBulkRequest = ApiPostCall({ + urlfromdata: true, }); - const MFARequest = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `/users/${userId}/authentication/methods`, - tenantFilter: userSettingsDefaults.currentTenant, - noPagination: true, - $top: 99, - }, - queryKey: `MFA-${userId}`, - waiting: waiting, - }); + useEffect(() => { + if (userId && userSettingsDefaults.currentTenant && !userBulkRequest.isSuccess) { + userBulkRequest.mutate({ + url: "/api/ListGraphBulkRequest", + data: { + Requests: [ + { + id: "userMemberOf", + url: `/users/${userId}/memberOf`, + method: "GET", + }, + { + id: "mfaDevices", + url: `/users/${userId}/authentication/methods?$top=99`, + method: "GET", + }, + { + id: "signInLogs", + url: `/auditLogs/signIns?$filter=(userId eq '${userId}')&$top=1`, + method: "GET", + }, + ], + tenantFilter: userSettingsDefaults.currentTenant, + noPaginateIds: ["signInLogs"], + }, + }); + } + }, [userId, userSettingsDefaults.currentTenant, userBulkRequest.isSuccess]); - const signInLogs = ApiGetCall({ - url: `/api/ListUserSigninLogs?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}&top=1`, - queryKey: `ListSignIns-${userId}`, - waiting: waiting, - }); + const bulkData = userBulkRequest?.data?.data ?? []; + const signInLogs = bulkData?.find((item) => item.id === "signInLogs")?.body?.value || []; + const userMemberOf = bulkData?.find((item) => item.id === "userMemberOf")?.body?.value || []; + const mfaDevices = bulkData?.find((item) => item.id === "mfaDevices")?.body?.value || []; + console.log(bulkData); // Set the title and subtitle for the layout const title = userRequest.isSuccess ? <>{userRequest.data?.[0]?.displayName} : "Loading..."; @@ -138,15 +149,15 @@ const Page = () => { icon: , text: ( + color="muted" + style={{ paddingLeft: 0 }} + size="small" + href={`https://entra.microsoft.com/${userSettingsDefaults.currentTenant}/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/${userId}`} + target="_blank" + rel="noopener noreferrer" + > + View in Entra + ), }, ] @@ -159,8 +170,8 @@ const Page = () => { let conditionalAccessPoliciesItems = []; let mfaDevicesItems = []; - if (signInLogs.isSuccess && signInLogs.data && signInLogs.data.length > 0) { - const signInData = signInLogs.data[0]; + if (signInLogs.length > 0) { + const signInData = signInLogs[0]; signInLogItem = { id: 1, @@ -330,12 +341,12 @@ const Page = () => { }, ]; } - } else if (signInLogs.isError) { + } else if (userBulkRequest.isError) { signInLogItem = { id: 1, cardLabelBox: "!", text: "Error loading sign-in logs. Do you have a P1 license?", - subtext: signInLogs.error.message, + subtext: userBulkRequest.error.message, statusColor: "error.main", statusText: "Error", propertyItems: [], @@ -347,13 +358,13 @@ const Page = () => { id: 1, cardLabelBox: "!", text: "Error loading conditional access policies. Do you have a P1 license?", - subtext: signInLogs.error.message, + subtext: userBulkRequest.error.message, statusColor: "error.main", statusText: "Error", propertyItems: [], }, ]; - } else if (signInLogs.isSuccess && (!signInLogs.data || signInLogs.data.length === 0)) { + } else if (signInLogs.length === 0) { signInLogItem = { id: 1, cardLabelBox: "-", @@ -380,16 +391,14 @@ const Page = () => { } // Prepare MFA devices items - if (MFARequest.isSuccess && MFARequest.data) { - const mfaResults = MFARequest.data.Results || []; - + if (mfaDevices.length > 0) { // Exclude password authentication method - const mfaDevices = mfaResults.filter( + const mfaDevicesFiltered = mfaDevices.filter( (method) => method["@odata.type"] !== "#microsoft.graph.passwordAuthenticationMethod" ); - if (mfaDevices.length > 0) { - mfaDevicesItems = mfaDevices.map((device, index) => ({ + if (mfaDevicesFiltered.length > 0) { + mfaDevicesItems = mfaDevicesFiltered.map((device, index) => ({ id: index, cardLabelBox: { cardLabelBoxHeader: , @@ -433,20 +442,20 @@ const Page = () => { }, ]; } - } else if (MFARequest.isError) { + } else if (userBulkRequest.isError) { // Error fetching MFA devices mfaDevicesItems = [ { id: 1, cardLabelBox: "!", text: "Error loading MFA devices", - subtext: MFARequest.error.message, + subtext: userBulkRequest.error.message, statusColor: "error.main", statusText: "Error", propertyItems: [], }, ]; - } else if (MFARequest.isSuccess && (!MFARequest.data || !MFARequest.data.Results)) { + } else if (mfaDevices.length === 0) { // No MFA devices data available mfaDevicesItems = [ { @@ -461,54 +470,33 @@ const Page = () => { ]; } - const groupMembershipItems = userMemberOf.isSuccess - ? [ - { - id: 1, - cardLabelBox: { - cardLabelBoxHeader: , - }, - text: "Groups", - subtext: "List of groups the user is a member of", - table: { - title: "Group Memberships", - hideTitle: true, - actions: [ - { - icon: , - label: "Edit Group", - link: "/identity/administration/groups/edit?groupId=[id]", - }, - ], - data: userMemberOf?.data?.Results.filter( - (item) => item?.["@odata.type"] === "#microsoft.graph.group" - ), - simpleColumns: ["displayName", "groupTypes", "securityEnabled", "mailEnabled"], - }, - }, - ] - : []; + const groupMembershipItems = userMemberOf + .filter((item) => item["@odata.type"] === "#microsoft.graph.group") + .map((group, index) => ({ + id: index, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: group.displayName, + subtext: "Group", + propertyItems: [ + { label: "Group Types", value: group.groupTypes.join(", ") || "N/A" }, + { label: "Security Enabled", value: group.securityEnabled ? "Yes" : "No" }, + { label: "Mail Enabled", value: group.mailEnabled ? "Yes" : "No" }, + ], + })); - const roleMembershipItems = userMemberOf.isSuccess - ? [ - { - id: 1, - cardLabelBox: { - cardLabelBoxHeader: , - }, - text: "Admin Roles", - subtext: "List of roles the user is a member of", - table: { - title: "Admin Roles", - hideTitle: true, - data: userMemberOf?.data?.Results.filter( - (item) => item?.["@odata.type"] === "#microsoft.graph.directoryRole" - ), - simpleColumns: ["displayName", "description"], - }, - }, - ] - : []; + const roleMembershipItems = userMemberOf + .filter((item) => item["@odata.type"] === "#microsoft.graph.directoryRole") + .map((role, index) => ({ + id: index, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: role.displayName, + subtext: "Admin Role", + propertyItems: [{ label: "Description", value: role.description || "N/A" }], + })); return ( { Latest Logon Applied Conditional Access Policies 0 ? true : false} /> Multi-Factor Authentication Devices 0 ? true : false} /> Memberships 0 ? true : false} - /> - 0 ? true : false} /> + 0 ? true : false} + /> From a15a452ad683035a5e40d64f359501b411df5231 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 6 Mar 2025 18:10:26 -0500 Subject: [PATCH 11/13] fix: pretty responses on user page --- .../administration/users/user/index.jsx | 134 +++++++++++++----- 1 file changed, 95 insertions(+), 39 deletions(-) diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 0e6aa451885e..b36347b4a1f3 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -24,6 +24,7 @@ const CippMap = dynamic(() => import("/src/components/CippComponents/CippMap"), import { Button, Dialog, DialogTitle, DialogContent, IconButton } from "@mui/material"; import { Close } from "@mui/icons-material"; import { CippPropertyList } from "../../../../../components/CippComponents/CippPropertyList"; +import { CippCodeBlock } from "../../../../../components/CippComponents/CippCodeBlock"; const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { return ( @@ -119,10 +120,13 @@ const Page = () => { }, [userId, userSettingsDefaults.currentTenant, userBulkRequest.isSuccess]); const bulkData = userBulkRequest?.data?.data ?? []; - const signInLogs = bulkData?.find((item) => item.id === "signInLogs")?.body?.value || []; - const userMemberOf = bulkData?.find((item) => item.id === "userMemberOf")?.body?.value || []; - const mfaDevices = bulkData?.find((item) => item.id === "mfaDevices")?.body?.value || []; - console.log(bulkData); + const signInLogsData = bulkData?.find((item) => item.id === "signInLogs"); + const userMemberOfData = bulkData?.find((item) => item.id === "userMemberOf"); + const mfaDevicesData = bulkData?.find((item) => item.id === "mfaDevices"); + + const signInLogs = signInLogsData?.body?.value || []; + const userMemberOf = userMemberOfData?.body?.value || []; + const mfaDevices = mfaDevicesData?.body?.value || []; // Set the title and subtitle for the layout const title = userRequest.isSuccess ? <>{userRequest.data?.[0]?.displayName} : "Loading..."; @@ -341,12 +345,12 @@ const Page = () => { }, ]; } - } else if (userBulkRequest.isError) { + } else if (signInLogsData?.status !== 200) { signInLogItem = { id: 1, cardLabelBox: "!", text: "Error loading sign-in logs. Do you have a P1 license?", - subtext: userBulkRequest.error.message, + subtext: signInLogsData?.error?.message || "Unknown error", statusColor: "error.main", statusText: "Error", propertyItems: [], @@ -358,7 +362,7 @@ const Page = () => { id: 1, cardLabelBox: "!", text: "Error loading conditional access policies. Do you have a P1 license?", - subtext: userBulkRequest.error.message, + subtext: signInLogsData?.error?.message || "Unknown error", statusColor: "error.main", statusText: "Error", propertyItems: [], @@ -373,7 +377,21 @@ const Page = () => { "There are no sign-in logs for this user, or you do not have a P1 license to detect this data.", statusColor: "warning.main", statusText: "No Data", - propertyItems: [], + propertyItems: [ + { + label: "Error", + value: signInLogsData?.error?.message || "Unknown error", + }, + { + label: "Inner Error", + value: ( + + ), + }, + ], }; conditionalAccessPoliciesItems = [ @@ -442,17 +460,34 @@ const Page = () => { }, ]; } - } else if (userBulkRequest.isError) { + } else if (mfaDevicesData?.status !== 200) { // Error fetching MFA devices mfaDevicesItems = [ { id: 1, cardLabelBox: "!", text: "Error loading MFA devices", - subtext: userBulkRequest.error.message, + subtext: `Status code: ${mfaDevicesData?.status}`, statusColor: "error.main", statusText: "Error", - propertyItems: [], + propertyItems: [ + { + label: "Error", + value: mfaDevicesData?.body?.error?.message || "Unknown Error", + }, + { + label: "Inner Error", + value: ( + + ), + }, + ], }, ]; } else if (mfaDevices.length === 0) { @@ -471,32 +506,53 @@ const Page = () => { } const groupMembershipItems = userMemberOf - .filter((item) => item["@odata.type"] === "#microsoft.graph.group") - .map((group, index) => ({ - id: index, - cardLabelBox: { - cardLabelBoxHeader: , - }, - text: group.displayName, - subtext: "Group", - propertyItems: [ - { label: "Group Types", value: group.groupTypes.join(", ") || "N/A" }, - { label: "Security Enabled", value: group.securityEnabled ? "Yes" : "No" }, - { label: "Mail Enabled", value: group.mailEnabled ? "Yes" : "No" }, - ], - })); + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Groups", + subtext: "List of groups the user is a member of", + table: { + title: "Group Memberships", + hideTitle: true, + actions: [ + { + icon: , + label: "Edit Group", + link: "/identity/administration/groups/edit?groupId=[id]", + }, + ], + data: userMemberOf?.filter( + (item) => item?.["@odata.type"] === "#microsoft.graph.group" + ), + simpleColumns: ["displayName", "groupTypes", "securityEnabled", "mailEnabled"], + }, + }, + ] + : []; const roleMembershipItems = userMemberOf - .filter((item) => item["@odata.type"] === "#microsoft.graph.directoryRole") - .map((role, index) => ({ - id: index, - cardLabelBox: { - cardLabelBoxHeader: , - }, - text: role.displayName, - subtext: "Admin Role", - propertyItems: [{ label: "Description", value: role.description || "N/A" }], - })); + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Admin Roles", + subtext: "List of roles the user is a member of", + table: { + title: "Admin Roles", + hideTitle: true, + data: userMemberOf?.filter( + (item) => item?.["@odata.type"] === "#microsoft.graph.directoryRole" + ), + simpleColumns: ["displayName", "description"], + }, + }, + ] + : []; return ( { Memberships 0 ? true : false} + items={groupMembershipItems} + isCollapsible={groupMembershipItems.length > 0 ? true : false} /> 0 ? true : false} + items={roleMembershipItems} + isCollapsible={roleMembershipItems.length > 0 ? true : false} /> From c77f47aeecd51949c1848c356fd770b130bfbfd5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 6 Mar 2025 18:33:59 -0500 Subject: [PATCH 12/13] Update version.json --- public/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/version.json b/public/version.json index c0ab80971e47..ba547756572f 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "7.3.1" + "version": "7.3.2" } From ab5a1ec9ea45ed0c69a36f6dde34c99684949f56 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 6 Mar 2025 18:41:00 -0500 Subject: [PATCH 13/13] Update config.js --- src/layouts/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/config.js b/src/layouts/config.js index e7093b9f1883..812b5639f999 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -463,7 +463,7 @@ export const nativeMenuItems = [ ), items: [ { title: "Application Settings", path: "/cipp/settings", roles: ["admin", "superadmin"] }, - { title: "Logbook", path: "/cipp/logs", roles: ["admin", "superadmin"] }, + { title: "Logbook", path: "/cipp/logs", roles: ["editor", "admin", "superadmin"] }, { title: "SAM Setup Wizard", path: "/onboarding", roles: ["admin", "superadmin"] }, { title: "Integrations", path: "/cipp/integrations", roles: ["admin", "superadmin"] }, {