{content}
diff --git a/src/components/CippWizard/CippWizardAppApproval.jsx b/src/components/CippWizard/CippWizardAppApproval.jsx
index a32eef3a32de..2502fb1a3103 100644
--- a/src/components/CippWizard/CippWizardAppApproval.jsx
+++ b/src/components/CippWizard/CippWizardAppApproval.jsx
@@ -1,51 +1,146 @@
-import { Stack } from "@mui/material";
+import { Stack, Typography, Alert, Box } from "@mui/material";
import CippWizardStepButtons from "./CippWizardStepButtons";
import { Grid } from "@mui/system";
import CippFormComponent from "../CippComponents/CippFormComponent";
import { getCippValidator } from "../../utils/get-cipp-validator";
import { CippFormCondition } from "../CippComponents/CippFormCondition";
+import { useEffect } from "react";
+import CippPermissionPreview from "../CippComponents/CippPermissionPreview";
+import { useWatch } from "react-hook-form";
+import { CippPropertyListCard } from "../CippCards/CippPropertyListCard";
export const CippWizardAppApproval = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props;
+ // Watch for the selected template to access permissions
+ const selectedTemplate = useWatch({
+ control: formControl.control,
+ name: "selectedTemplate",
+ });
+
return (
-
-
-
+
+ {/* Mode Selector */}
+
+
+ {/* Template Mode */}
+
+
+
+ Select an app approval template to deploy. Templates contain predefined permissions that
+ will be applied to the application.
+
getCippValidator(value, "guid"),
+ type="autoComplete"
+ name="selectedTemplate"
+ label="Select App Template"
+ api={{
+ url: "/api/ListAppApprovalTemplates",
+ queryKey: "appApprovalTemplates",
+ labelField: (item) => `${item.TemplateName}`,
+ valueField: "TemplateId",
+ addedField: {
+ AppId: "AppId",
+ AppName: "AppName",
+ PermissionSetId: "PermissionSetId",
+ PermissionSetName: "PermissionSetName",
+ Permissions: "Permissions",
+ },
+ showRefresh: true,
}}
- name="AppId"
+ validators={{ required: "A template is required" }}
formControl={formControl}
+ multiple={false}
/>
-
-
-
+
+ {selectedTemplate?.addedFields?.AppName && (
+
+
+
+
+ )}
+
+
+
+ {/* Manual Mode */}
+
+
+ getCippValidator(value, "guid"),
+ }}
+ name="AppId"
+ formControl={formControl}
+ />
+
+
+
+
+
+
+
+
{
@@ -8,7 +9,7 @@ export const CippWizardAutopilotOptions = (props) => {
<>
-
+
{
<>
-
+
{
formControl={formControl}
/>
-
+
{
{fields.map((field) => (
<>
-
+
{
>
))}
-
+
@@ -116,7 +108,7 @@ export const CippWizardCSVImport = (props) => {
{!manualFields && (
<>
-
+
@@ -127,7 +119,7 @@ export const CippWizardCSVImport = (props) => {
{fields.map((field) => (
-
+
{
- const { postUrl, lastStep, formControl, onPreviousStep, onNextStep, currentStep } = props;
- const formValues = formControl.getValues();
- const formEntries = Object.entries(formValues);
- //remove all entries in "blacklist" from showing on confirmation page
- const blacklist = [
- "selectedOption",
- "GUID",
- "ID",
- "noSubmitButton",
- "RAWJson",
- "TemplateList",
- "addrow",
- ];
-
- const tenantEntry = formEntries.find(([key]) => key === "tenantFilter" || key === "tenant");
- const userEntry = formEntries.find(([key]) =>
- ["user", "userPrincipalName", "username"].includes(key)
- );
- const filteredEntries = formEntries.filter(
- ([key]) =>
- !blacklist.includes(key) &&
- key !== "tenantFilter" &&
- key !== "tenant" &&
- !["user", "userPrincipalName", "username"].includes(key)
- );
-
- const halfIndex = Math.ceil(filteredEntries.length / 2);
- const firstHalf = filteredEntries.slice(0, halfIndex);
- const secondHalf = filteredEntries.slice(halfIndex);
-
- if (tenantEntry) {
- firstHalf.unshift(tenantEntry);
- }
-
- if (userEntry) {
- secondHalf.unshift(userEntry);
- }
-
- return (
-
-
-
-
-
- {firstHalf.map(([key, value]) => {
- const formattedValue = getCippFormatting(value, key);
- const label = getCippTranslation(key);
- return ;
- })}
-
-
-
-
- {secondHalf.map(([key, value]) => {
- const formattedValue = getCippFormatting(value, key);
- const label = getCippTranslation(key);
- return ;
- })}
-
-
-
-
-
-
-
- );
-};
-
-export default CippWizardConfirmation;
diff --git a/src/components/CippWizard/CippWizardConfirmation.jsx b/src/components/CippWizard/CippWizardConfirmation.jsx
new file mode 100644
index 000000000000..cd675d9be406
--- /dev/null
+++ b/src/components/CippWizard/CippWizardConfirmation.jsx
@@ -0,0 +1,110 @@
+import { Card, Stack, Typography } from "@mui/material";
+import { Grid } from "@mui/system";
+import { PropertyList } from "../property-list";
+import { PropertyListItem } from "../property-list-item";
+import CippWizardStepButtons from "./CippWizardStepButtons";
+import { getCippTranslation } from "../../utils/get-cipp-translation";
+import { getCippFormatting } from "../../utils/get-cipp-formatting";
+
+export const CippWizardConfirmation = (props) => {
+ const { postUrl, lastStep, formControl, onPreviousStep, onNextStep, currentStep } = props;
+ const formValues = formControl.getValues();
+ const formEntries = Object.entries(formValues);
+
+ const blacklist = [
+ "selectedOption",
+ "GDAPAuth",
+ "SAMWizard",
+ "GUID",
+ "ID",
+ "noSubmitButton",
+ "RAWJson",
+ "TemplateList",
+ "addrow",
+ ];
+
+ // Filter out null values and undefined values which could be from hidden conditional fields
+ const filteredFormEntries = formEntries.filter(
+ ([_, value]) => value !== null && value !== undefined
+ );
+
+ const tenantEntry = filteredFormEntries.find(
+ ([key]) => key === "tenantFilter" || key === "tenant"
+ );
+ const userEntry = filteredFormEntries.find(([key]) =>
+ ["user", "userPrincipalName", "username"].includes(key)
+ );
+
+ const filteredEntries = formEntries.filter(
+ ([key]) =>
+ !blacklist.includes(key) &&
+ key !== "tenantFilter" &&
+ key !== "tenant" &&
+ !["user", "userPrincipalName", "username"].includes(key)
+ );
+
+ const halfIndex = Math.ceil(filteredEntries.length / 2);
+ const firstHalf = filteredEntries.slice(0, halfIndex);
+ const secondHalf = filteredEntries.slice(halfIndex);
+
+ if (tenantEntry) {
+ firstHalf.unshift(tenantEntry);
+ }
+
+ if (userEntry) {
+ secondHalf.unshift(userEntry);
+ }
+
+ return (
+
+ {firstHalf.length === 0 ? (
+
+
+
+ You've completed the steps in this wizard. Hit submit to save your changes.
+
+
+
+ ) : (
+
+
+
+
+ {firstHalf.map(([key, value]) => (
+
+ ))}
+
+
+
+
+ {secondHalf.map(([key, value]) => (
+
+ ))}
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default CippWizardConfirmation;
diff --git a/src/components/CippWizard/CippWizardGroupTemplates.jsx b/src/components/CippWizard/CippWizardGroupTemplates.jsx
index 215d22a0509f..2ac2d6435cd6 100644
--- a/src/components/CippWizard/CippWizardGroupTemplates.jsx
+++ b/src/components/CippWizard/CippWizardGroupTemplates.jsx
@@ -42,7 +42,8 @@ export const CippWizardGroupTemplates = (props) => {
excludeTenantFilter: true,
url: "/api/ListGroupTemplates",
queryKey: "ListGroupTemplates",
- labelField: (option) => `${option.Displayname} (${option.groupType})`,
+ labelField: (option) =>
+ `${option.Displayname || option.displayName} (${option.groupType})`,
valueField: "GUID",
addedField: {
groupType: "groupType",
diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx
index 4faeb57318c2..ea33766e9dc7 100644
--- a/src/components/CippWizard/CippWizardOffboarding.jsx
+++ b/src/components/CippWizard/CippWizardOffboarding.jsx
@@ -277,7 +277,7 @@ export const CippWizardOffboarding = (props) => {
{showAlert && (
- You have selected more than 3 users. This offboarding must be scheduled.
+ You have selected more than 2 users. This offboarding must be scheduled.
)}
@@ -286,7 +286,7 @@ export const CippWizardOffboarding = (props) => {
-
+
{
compareType="is"
compareValue={true}
>
-
+
Scheduled Offboarding Date
{
/>
-
+
Send results to:
{
const newData = {};
Object.keys(values).forEach((key) => {
const value = values[key];
- if (replacementBehaviour !== "removeNulls") {
+ // Only add non-null values if removeNulls is specified
+ if (replacementBehaviour !== "removeNulls" || value !== null) {
newData[key] = value;
- } else if (row[value] !== undefined) {
- newData[key] = row[value];
}
});
sendForm.mutate({ url: postUrl, data: newData });
diff --git a/src/components/CippWizard/CustomerForm.jsx b/src/components/CippWizard/CustomerForm.jsx
index e441faa0d932..aa4c14057886 100644
--- a/src/components/CippWizard/CustomerForm.jsx
+++ b/src/components/CippWizard/CustomerForm.jsx
@@ -1,4 +1,5 @@
-import { Grid } from "@mui/material";
+import "@mui/material";
+import { Grid } from "@mui/system";
import CippFormComponent from "../CippComponents/CippFormComponent";
export const CustomerForm = (props) => {
@@ -69,7 +70,7 @@ export const CustomerForm = (props) => {
return (
{fields.map((field, index) => (
-
+
{
{steps.map((step) => (
- {step.title}
+
+ {`Step ${steps.indexOf(step) ? steps.indexOf(step) + 1 : 1}`}
+
{step.description}
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 011886bc4499..3541eeda2d96 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -1,35 +1,62 @@
import { ApiGetCall } from "../api/ApiCall.jsx";
import UnauthenticatedPage from "../pages/unauthenticated.js";
+import LoadingPage from "../pages/loading.js";
export const PrivateRoute = ({ children, routeType }) => {
const {
data: profile,
error,
isLoading,
+ isSuccess,
+ refetch,
} = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
+ });
+
+ const session = ApiGetCall({
+ url: "/.auth/me",
+ queryKey: "authmeswa",
refetchOnWindowFocus: true,
staleTime: 120000, // 2 minutes
});
- if (isLoading) {
- return "Loading...";
+ // Check if the session is still loading before determining authentication status
+ if (session.isLoading || isLoading) {
+ return ;
+ }
+
+ // if not logged into swa
+ if (null === session?.data?.clientPrincipal || session?.data === undefined) {
+ return ;
}
let roles = null;
- if (null !== profile?.clientPrincipal) {
- roles = profile?.clientPrincipal.userRoles;
- } else if (null === profile?.clientPrincipal) {
+
+ if (
+ session?.isSuccess &&
+ isSuccess &&
+ undefined !== profile &&
+ session?.data?.clientPrincipal?.userDetails &&
+ profile?.userDetails &&
+ session?.data?.clientPrincipal?.userDetails !== profile?.userDetails
+ ) {
+ // refetch the profile if the user details are different
+ refetch();
+ }
+
+ if (null !== profile?.clientPrincipal && undefined !== profile) {
+ roles = profile?.clientPrincipal?.userRoles;
+ } else if (null === profile?.clientPrincipal || undefined === profile) {
return ;
}
if (null === roles) {
return ;
} else {
const blockedRoles = ["anonymous", "authenticated"];
- const userRoles = roles.filter((role) => !blockedRoles.includes(role));
+ const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? [];
const isAuthenticated = userRoles.length > 0 && !error;
- const isAdmin = roles.includes("admin");
+ const isAdmin = roles.includes("admin") || roles.includes("superadmin");
if (routeType === "admin") {
return !isAdmin ? : children;
} else {
diff --git a/src/components/linearProgressWithLabel.jsx b/src/components/linearProgressWithLabel.jsx
index ffac26c2086b..f01031da45ca 100644
--- a/src/components/linearProgressWithLabel.jsx
+++ b/src/components/linearProgressWithLabel.jsx
@@ -6,7 +6,7 @@ export const LinearProgressWithLabel = (props) => {
- {`${Math.round(props.value)}% ${props?.addedLabel ?? ""}`}
+ {`${Math.round(props.value)}% ${props?.addedLabel ?? ""}`}
);
};
diff --git a/src/data/Extensions.json b/src/data/Extensions.json
index 3c61cbbba574..badbfc046cf9 100644
--- a/src/data/Extensions.json
+++ b/src/data/Extensions.json
@@ -841,7 +841,7 @@
"logo": "/assets/integrations/github.png",
"logoDark": "/assets/integrations/github_dark.png",
"description": "Enable the GitHub integration to manage your repositories from CIPP.",
- "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositorities functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.",
+ "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositories functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.",
"links": [
{
"name": "GitHub Token",
diff --git a/src/data/alerts.json b/src/data/alerts.json
index 4779d23e58e1..e71be4e1547f 100644
--- a/src/data/alerts.json
+++ b/src/data/alerts.json
@@ -28,6 +28,15 @@
"inputName": "InactiveLicensedUsersExcludeDisabled",
"recommendedRunInterval": "1d"
},
+ {
+ "name": "EntraConnectSyncStatus",
+ "label": "Alert if Entra Connect sync is enabled and has not run in the last X hours",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Hours(Default:72)",
+ "inputName": "EntraConnectSyncStatusHours",
+ "recommendedRunInterval": "1d"
+ },
{
"name": "QuotaUsed",
"label": "Alert on % mailbox quota used",
diff --git a/src/data/cipp-roles.json b/src/data/cipp-roles.json
new file mode 100644
index 000000000000..f95e32fa18c6
--- /dev/null
+++ b/src/data/cipp-roles.json
@@ -0,0 +1,23 @@
+{
+ "readonly": {
+ "include": ["*.Read"],
+ "exclude": ["CIPP.SuperAdmin.*"]
+ },
+ "editor": {
+ "include": ["*.Read", "*.ReadWrite"],
+ "exclude": [
+ "CIPP.SuperAdmin.*",
+ "CIPP.Admin.*",
+ "CIPP.AppSettings.*",
+ "Tenant.Standards.ReadWrite"
+ ]
+ },
+ "admin": {
+ "include": ["*"],
+ "exclude": ["CIPP.SuperAdmin.*"]
+ },
+ "superadmin": {
+ "include": ["*"],
+ "exclude": []
+ }
+}
diff --git a/src/data/languageList.json b/src/data/languageList.json
index fea7ddddc8ca..c4e742af3a1e 100644
--- a/src/data/languageList.json
+++ b/src/data/languageList.json
@@ -5,6 +5,96 @@
"tag": "ar-SA",
"LCID": "1025"
},
+ {
+ "language": "Arabic",
+ "Geographic area": "Algeria",
+ "tag": "ar-DZ",
+ "LCID": "5121"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Egypt",
+ "tag": "ar-EG",
+ "LCID": "3073"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Bahrain",
+ "tag": "ar-BH",
+ "LCID": "15361"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Iraq",
+ "tag": "ar-IQ",
+ "LCID": "2049"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Jordan",
+ "tag": "ar-JO",
+ "LCID": "11265"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Kuwait",
+ "tag": "ar-KW",
+ "LCID": "13313"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Lebanon",
+ "tag": "ar-LB",
+ "LCID": "12289"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Libya",
+ "tag": "ar-LY",
+ "LCID": "4097"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Morocco",
+ "tag": "ar-MA",
+ "LCID": "6145"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Oman",
+ "tag": "ar-OM",
+ "LCID": "8193"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Qatar",
+ "tag": "ar-QA",
+ "LCID": "16385"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Syria",
+ "tag": "ar-SY",
+ "LCID": "10241"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Tunisia",
+ "tag": "ar-TN",
+ "LCID": "7169"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "UAE",
+ "tag": "ar-AE",
+ "LCID": "14337"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Yemen",
+ "tag": "ar-YE",
+ "LCID": "9217"
+ },
{
"language": "Bulgarian",
"Geographic area": "Bulgaria",
@@ -23,6 +113,12 @@
"tag": "zh-TW",
"LCID": "1028"
},
+ {
+ "language": "Chinese",
+ "Geographic area": "Hong Kong SAR",
+ "tag": "zh-HK",
+ "LCID": "3076"
+ },
{
"language": "Croatian",
"Geographic area": "Croatia",
@@ -53,6 +149,42 @@
"tag": "en-US",
"LCID": "1033"
},
+ {
+ "language": "English",
+ "Geographic area": "Australia",
+ "tag": "en-AU",
+ "LCID": "3081"
+ },
+ {
+ "language": "English",
+ "Geographic area": "United Kingdom",
+ "tag": "en-GB",
+ "LCID": "2057"
+ },
+ {
+ "language": "English",
+ "Geographic area": "New Zealand",
+ "tag": "en-NZ",
+ "LCID": "5129"
+ },
+ {
+ "language": "English",
+ "Geographic area": "Canada",
+ "tag": "en-CA",
+ "LCID": "4105"
+ },
+ {
+ "language": "English",
+ "Geographic area": "South Africa",
+ "tag": "en-ZA",
+ "LCID": "7177"
+ },
+ {
+ "language": "English",
+ "Geographic area": "Singapore",
+ "tag": "en-SG",
+ "LCID": "4100"
+ },
{
"language": "Estonian",
"Geographic area": "Estonia",
@@ -71,12 +203,30 @@
"tag": "fr-FR",
"LCID": "1036"
},
+ {
+ "language": "French",
+ "Geographic area": "Canada",
+ "tag": "fr-CA",
+ "LCID": "3084"
+ },
+ {
+ "language": "French",
+ "Geographic area": "Switzerland",
+ "tag": "fr-CH",
+ "LCID": "4108"
+ },
{
"language": "German",
"Geographic area": "Germany",
"tag": "de-DE",
"LCID": "1031"
},
+ {
+ "language": "German",
+ "Geographic area": "Switzerland",
+ "tag": "de-CH",
+ "LCID": "2055"
+ },
{
"language": "Greek",
"Geographic area": "Greece",
@@ -155,6 +305,12 @@
"tag": "nb-NO",
"LCID": "1044"
},
+ {
+ "language": "Persian",
+ "Geographic area": "Iran",
+ "tag": "fa-IR",
+ "LCID": "1065"
+ },
{
"language": "Polish",
"Geographic area": "Poland",
@@ -209,6 +365,108 @@
"tag": "es-ES",
"LCID": "3082"
},
+ {
+ "language": "Spanish",
+ "Geographic area": "Argentina",
+ "tag": "es-AR",
+ "LCID": "11274"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Bolivia",
+ "tag": "es-BO",
+ "LCID": "16394"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Chile",
+ "tag": "es-CL",
+ "LCID": "13322"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Colombia",
+ "tag": "es-CO",
+ "LCID": "9226"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Costa Rica",
+ "tag": "es-CR",
+ "LCID": "5130"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Dominican Republic",
+ "tag": "es-DO",
+ "LCID": "7178"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Ecuador",
+ "tag": "es-EC",
+ "LCID": "12298"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "El Salvador",
+ "tag": "es-SV",
+ "LCID": "17418"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Guatemala",
+ "tag": "es-GT",
+ "LCID": "4106"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Honduras",
+ "tag": "es-HN",
+ "LCID": "18442"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Mexico",
+ "tag": "es-MX",
+ "LCID": "2058"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Nicaragua",
+ "tag": "es-NI",
+ "LCID": "19466"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Panama",
+ "tag": "es-PA",
+ "LCID": "6154"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Paraguay",
+ "tag": "es-PY",
+ "LCID": "15370"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Peru",
+ "tag": "es-PE",
+ "LCID": "10250"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Uruguay",
+ "tag": "es-UY",
+ "LCID": "14346"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Venezuela",
+ "tag": "es-VE",
+ "LCID": "8202"
+ },
{
"language": "Swedish",
"Geographic area": "Sweden",
@@ -229,10 +487,16 @@
},
{
"language": "Ukrainian",
- "Geographic area": "Ukrainian",
+ "Geographic area": "Ukraine",
"tag": "uk-UA",
"LCID": "1058"
},
+ {
+ "language": "Urdu",
+ "Geographic area": "Pakistan",
+ "tag": "ur-PK",
+ "LCID": "1056"
+ },
{
"language": "Vietnamese",
"Geographic area": "Vietnam",
diff --git a/src/data/standards.json b/src/data/standards.json
index 6abcb77d9791..4dba29061348 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -336,10 +336,53 @@
"helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.",
"docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.",
"addedComponent": [
+ {
+ "type": "select",
+ "multiple": false,
+ "creatable": false,
+ "label": "App Approval Mode",
+ "name": "standards.AppDeploy.mode",
+ "options": [
+ {
+ "label": "Template",
+ "value": "template"
+ },
+ {
+ "label": "Copy Permissions",
+ "value": "copy"
+ }
+ ]
+ },
+ {
+ "type": "autoComplete",
+ "multiple": true,
+ "creatable": false,
+ "label": "Select Applications",
+ "name": "standards.AppDeploy.templateIds",
+ "api": {
+ "url": "/api/ListAppApprovalTemplates",
+ "labelField": "TemplateName",
+ "valueField": "TemplateId",
+ "queryKey": "StdAppApprovalTemplateList",
+ "addedField": {
+ "AppId": "AppId"
+ }
+ },
+ "condition": {
+ "field": "standards.AppDeploy.mode",
+ "compareType": "is",
+ "compareValue": "template"
+ }
+ },
{
"type": "textField",
"name": "standards.AppDeploy.appids",
- "label": "Application IDs, comma separated"
+ "label": "Application IDs, comma separated",
+ "condition": {
+ "field": "standards.AppDeploy.mode",
+ "compareType": "isNot",
+ "compareValue": "template"
+ }
}
],
"label": "Deploy Application",
@@ -1371,6 +1414,60 @@
"powershellEquivalent": "Set-MailboxFolderPermission",
"recommendedBy": []
},
+ {
+ "name": "standards.EXOOutboundSpamLimits",
+ "cat": "Exchange Standards",
+ "tag": ["CIS"],
+ "helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ",
+ "docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.",
+ "addedComponent": [
+ {
+ "type": "number",
+ "name": "standards.EXOOutboundSpamLimits.RecipientLimitExternalPerHour",
+ "label": "External Recipient Limit Per Hour",
+ "defaultValue": 400
+ },
+ {
+ "type": "number",
+ "name": "standards.EXOOutboundSpamLimits.RecipientLimitInternalPerHour",
+ "label": "Internal Recipient Limit Per Hour",
+ "defaultValue": 800
+ },
+ {
+ "type": "number",
+ "name": "standards.EXOOutboundSpamLimits.RecipientLimitPerDay",
+ "label": "Daily Recipient Limit",
+ "defaultValue": 800
+ },
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "name": "standards.EXOOutboundSpamLimits.ActionWhenThresholdReached",
+ "label": "Action When Threshold Reached",
+ "options": [
+ {
+ "label": "Alert",
+ "value": "Alert"
+ },
+ {
+ "label": "Block User",
+ "value": "BlockUser"
+ },
+ {
+ "label": "Block user from sending mail for the rest of the day",
+ "value": "BlockUserForToday"
+ }
+ ]
+ }
+ ],
+ "label": "Set Exchange Outbound Spam Limits",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-05-13",
+ "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy",
+ "recommendedBy": ["CIPP", "CIS"]
+ },
{
"name": "standards.DisableExternalCalendarSharing",
"cat": "Exchange Standards",
@@ -1823,6 +1920,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for Spoof",
"name": "standards.AntiPhishPolicy.SpoofQuarantineTag",
"options": [
@@ -1863,6 +1961,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for user impersonation",
"name": "standards.AntiPhishPolicy.TargetedUserQuarantineTag",
"options": [
@@ -1903,6 +2002,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for domain impersonation",
"name": "standards.AntiPhishPolicy.TargetedDomainQuarantineTag",
"options": [
@@ -1943,6 +2043,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Apply quarantine policy",
"name": "standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag",
"options": [
@@ -1997,6 +2098,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "QuarantineTag",
"name": "standards.SafeAttachmentPolicy.QuarantineTag",
"options": [
@@ -2023,7 +2125,12 @@
"type": "textField",
"name": "standards.SafeAttachmentPolicy.RedirectAddress",
"label": "Redirect Address",
- "required": false
+ "required": false,
+ "condition": {
+ "field": "standards.SafeAttachmentPolicy.Redirect",
+ "compareType": "is",
+ "compareValue": true
+ }
}
],
"label": "Default Safe Attachment Policy",
@@ -2083,6 +2190,13 @@
"required": false,
"label": "Phishing Simulation Urls",
"name": "standards.PhishingSimulations.PhishingSimUrls"
+ },
+ {
+ "type": "switch",
+ "label": "Remove extra urls",
+ "name": "standards.PhishingSimulations.RemoveExtraUrls",
+ "defaultValue": false,
+ "required": false
}
],
"label": "Phishing Simulation Configuration",
@@ -2123,6 +2237,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "QuarantineTag",
"name": "standards.MalwareFilterPolicy.QuarantineTag",
"options": [
@@ -2150,7 +2265,12 @@
"type": "textField",
"name": "standards.MalwareFilterPolicy.InternalSenderAdminAddress",
"required": false,
- "label": "Internal Sender Admin Address"
+ "label": "Internal Sender Admin Address",
+ "condition": {
+ "field": "standards.MalwareFilterPolicy.EnableInternalSenderAdminNotifications",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "switch",
@@ -2162,7 +2282,12 @@
"type": "textField",
"name": "standards.MalwareFilterPolicy.ExternalSenderAdminAddress",
"required": false,
- "label": "External Sender Admin Address"
+ "label": "External Sender Admin Address",
+ "condition": {
+ "field": "standards.MalwareFilterPolicy.EnableExternalSenderAdminNotifications",
+ "compareType": "is",
+ "compareValue": true
+ }
}
],
"label": "Default Malware Filter Policy",
@@ -2178,6 +2303,13 @@
"tag": [],
"helpText": "This adds allowed domains to the Spoof Intelligence Allow/Block List.",
"addedComponent": [
+ {
+ "type": "switch",
+ "label": "Remove extra domains from the allow list",
+ "name": "standards.PhishSimSpoofIntelligence.RemoveExtraDomains",
+ "defaultValue": false,
+ "required": false
+ },
{
"type": "autoComplete",
"multiple": true,
@@ -2228,7 +2360,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Spam Quarantine Tag",
"name": "standards.SpamFilterPolicy.SpamQuarantineTag",
"options": [
@@ -2268,7 +2400,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "High Confidence Spam Quarantine Tag",
"name": "standards.SpamFilterPolicy.HighConfidenceSpamQuarantineTag",
"options": [
@@ -2308,7 +2440,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Bulk Quarantine Tag",
"name": "standards.SpamFilterPolicy.BulkQuarantineTag",
"options": [
@@ -2348,7 +2480,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Phish Quarantine Tag",
"name": "standards.SpamFilterPolicy.PhishQuarantineTag",
"options": [
@@ -2370,7 +2502,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "High Confidence Phish Quarantine Tag",
"name": "standards.SpamFilterPolicy.HighConfidencePhishQuarantineTag",
"options": [
@@ -2448,7 +2580,12 @@
"creatable": true,
"required": false,
"name": "standards.SpamFilterPolicy.LanguageBlockList",
- "label": "Languages to block (uppercase ISO 639-1 two-letter)"
+ "label": "Languages to block (uppercase ISO 639-1 two-letter)",
+ "condition": {
+ "field": "standards.SpamFilterPolicy.EnableLanguageBlockList",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "switch",
@@ -2462,7 +2599,12 @@
"creatable": true,
"required": false,
"name": "standards.SpamFilterPolicy.RegionBlockList",
- "label": "Regions to block (uppercase ISO 3166-1 two-letter)"
+ "label": "Regions to block (uppercase ISO 3166-1 two-letter)",
+ "condition": {
+ "field": "standards.SpamFilterPolicy.EnableRegionBlockList",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "autoComplete",
@@ -2480,6 +2622,92 @@
"powershellEquivalent": "New-HostedContentFilterPolicy or Set-HostedContentFilterPolicy",
"recommendedBy": []
},
+ {
+ "name": "standards.QuarantineTemplate",
+ "cat": "Defender Standards",
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "tag": [],
+ "helpText": "This standard creates a Custom Quarantine Policies that can be used in Anti-Spam and all MDO365 policies. Quarantine Policies can be used to specify recipients permissions, enable end-user spam notifications, and specify the release action preference",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": true,
+ "name": "displayName",
+ "label": "Quarantine Display Name",
+ "required": true
+ },
+ {
+ "type": "switch",
+ "label": "Enable end-user spam notifications",
+ "name": "ESNEnabled",
+ "defaultValue": true,
+ "required": false
+ },
+ {
+ "type": "select",
+ "multiple": false,
+ "label": "Select release action preference",
+ "name": "ReleaseAction",
+ "options": [
+ {
+ "label": "Allow recipients to request a message to be released from quarantine",
+ "value": "PermissionToRequestRelease"
+ },
+ {
+ "label": "Allow recipients to release a message from quarantine",
+ "value": "PermissionToRelease"
+ }
+ ]
+ },
+ {
+ "type": "switch",
+ "label": "Include Messages From Blocked Sender Address",
+ "name": "IncludeMessagesFromBlockedSenderAddress",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to delete message",
+ "name": "PermissionToDelete",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to preview message",
+ "name": "PermissionToPreview",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to block Sender Address",
+ "name": "PermissionToBlockSender",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to whitelist Sender Address",
+ "name": "PermissionToAllowSender",
+ "defaultValue": false,
+ "required": false
+ }
+ ],
+ "label": "Custom Quarantine Policy",
+ "multiple": true,
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-05-16",
+ "powershellEquivalent": "Set-QuarantinePolicy or New-QuarantinePolicy",
+ "recommendedBy": []
+ },
{
"name": "standards.intuneDeviceRetirementDays",
"cat": "Intune Standards",
@@ -2621,9 +2849,18 @@
"label": "MDM User Scope?",
"type": "radio",
"options": [
- { "label": "All", "value": "all" },
- { "label": "None", "value": "none" },
- { "label": "Custom Group", "value": "selected" }
+ {
+ "label": "All",
+ "value": "all"
+ },
+ {
+ "label": "None",
+ "value": "none"
+ },
+ {
+ "label": "Custom Group",
+ "value": "selected"
+ }
]
},
{
@@ -3329,11 +3566,6 @@
"name": "standards.TeamsExternalAccessPolicy.EnableFederationAccess",
"label": "Allow communication from trusted organizations"
},
- {
- "type": "switch",
- "name": "standards.TeamsExternalAccessPolicy.EnablePublicCloudAccess",
- "label": "Allow user to communicate with Skype users"
- },
{
"type": "switch",
"name": "standards.TeamsExternalAccessPolicy.EnableTeamsConsumerAccess",
@@ -3359,11 +3591,6 @@
"name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer",
"label": "Allow users to communicate with other organizations"
},
- {
- "type": "switch",
- "name": "standards.TeamsFederationConfiguration.AllowPublicUsers",
- "label": "Allow users to communicate with Skype Users"
- },
{
"type": "autoComplete",
"required": true,
@@ -3716,11 +3943,26 @@
"label": "Who should this template be assigned to?",
"type": "radio",
"options": [
- { "label": "Do not assign", "value": "On" },
- { "label": "Assign to all users", "value": "allLicensedUsers" },
- { "label": "Assign to all devices", "value": "AllDevices" },
- { "label": "Assign to all users and devices", "value": "AllDevicesAndUsers" },
- { "label": "Assign to Custom Group", "value": "customGroup" }
+ {
+ "label": "Do not assign",
+ "value": "On"
+ },
+ {
+ "label": "Assign to all users",
+ "value": "allLicensedUsers"
+ },
+ {
+ "label": "Assign to all devices",
+ "value": "AllDevices"
+ },
+ {
+ "label": "Assign to all users and devices",
+ "value": "AllDevicesAndUsers"
+ },
+ {
+ "label": "Assign to Custom Group",
+ "value": "customGroup"
+ }
]
},
{
@@ -3795,10 +4037,22 @@
"label": "What state should we deploy this template in?",
"type": "radio",
"options": [
- { "value": "donotchange", "label": "Do not change state" },
- { "value": "Enabled", "label": "Set to enabled" },
- { "value": "Disabled", "label": "Set to disabled" },
- { "value": "enabledForReportingButNotEnforced", "label": "Set to report only" }
+ {
+ "value": "donotchange",
+ "label": "Do not change state"
+ },
+ {
+ "value": "Enabled",
+ "label": "Set to enabled"
+ },
+ {
+ "value": "Disabled",
+ "label": "Set to disabled"
+ },
+ {
+ "value": "enabledForReportingButNotEnforced",
+ "label": "Set to report only"
+ }
]
}
]
@@ -3849,6 +4103,7 @@
"api": {
"url": "/api/ListGroupTemplates",
"labelField": "Displayname",
+ "altLabelField": "displayName",
"valueField": "GUID",
"queryKey": "ListGroupTemplates"
}
diff --git a/src/data/timezoneList.json b/src/data/timezoneList.json
index d259bc80e65d..57238fe7eedc 100644
--- a/src/data/timezoneList.json
+++ b/src/data/timezoneList.json
@@ -189,7 +189,7 @@
"timezone": "(UTC+04:00) Astrakhan, Ulyanovsk"
},
{
- "timezone": "(UTC+04:00) Baku"
+ "timezone": "(UTC+04:00) Baku"
},
{
"timezone": "(UTC+04:00) Izhevsk, Samara"
diff --git a/src/hooks/use-securescore.js b/src/hooks/use-securescore.js
index 2394fa98ed0a..bea4554f4506 100644
--- a/src/hooks/use-securescore.js
+++ b/src/hooks/use-securescore.js
@@ -66,7 +66,7 @@ export function useSecureScore() {
complianceInformation: translation?.complianceInformation,
actionUrl: remediation
? //this needs to be updated to be a direct url to apply this standard.
- "/tenant/standards/list-applied-standards"
+ "/tenant/standards/list-standards"
: translation?.actionUrl,
remediation: remediation
? `1. Enable the CIPP Standard: ${remediation.label}`
diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js
index ab6b9a11155b..88830a672145 100644
--- a/src/layouts/account-popover.js
+++ b/src/layouts/account-popover.js
@@ -9,12 +9,14 @@ import SunIcon from "@heroicons/react/24/outline/SunIcon";
import {
Avatar,
Box,
+ CircularProgress,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Popover,
+ Skeleton,
Stack,
SvgIcon,
Typography,
@@ -38,10 +40,8 @@ export const AccountPopover = (props) => {
const popover = usePopover();
const orgData = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
const handleLogout = useCallback(async () => {
@@ -89,16 +89,22 @@ export const AccountPopover = (props) => {
<>
- {orgData.data?.Org?.Domain}
+ {orgData.data?.clientPrincipal?.userDetails?.split("@")?.[1]}
{orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"}
{orgData.data?.clientPrincipal?.userDetails && (
-
-
-
+ <>
+ {orgData?.isFetching ? (
+
+ ) : (
+
+
+
+ )}
+ >
)}
>
)}
diff --git a/src/layouts/config.js b/src/layouts/config.js
index ccf67d86e9ba..c853befc8a08 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -87,8 +87,8 @@ export const nativeMenuItems = [
},
{ title: "Audit Logs", path: "/tenant/administration/audit-logs" },
{
- title: "Enterprise Applications",
- path: "/tenant/administration/enterprise-apps",
+ title: "Applications",
+ path: "/tenant/administration/applications/enterprise-apps",
},
{ title: "Secure Score", path: "/tenant/administration/securescore" },
{
@@ -258,6 +258,7 @@ export const nativeMenuItems = [
path: "/endpoint/reports",
items: [
{ title: "Analytics Device Score", path: "/endpoint/reports/analyticsdevicescore" },
+ { title: "Work from anywhere", path: "/endpoint/reports/workfromanywhere" },
],
},
],
@@ -341,6 +342,10 @@ export const nativeMenuItems = [
title: "Connection filter templates",
path: "/email/spamfilter/list-connectionfilter-templates",
},
+ {
+ title: "Quarantine Policies",
+ path: "/email/spamfilter/list-quarantine-policies",
+ },
],
},
{
@@ -464,12 +469,12 @@ export const nativeMenuItems = [
items: [
{ title: "Application Settings", path: "/cipp/settings", roles: ["admin", "superadmin"] },
{ title: "Logbook", path: "/cipp/logs", roles: ["editor", "admin", "superadmin"] },
- { title: "SAM Setup Wizard", path: "/onboarding", roles: ["admin", "superadmin"] },
+ { title: "Setup Wizard", path: "/onboardingv2", roles: ["admin", "superadmin"] },
{ title: "Integrations", path: "/cipp/integrations", roles: ["admin", "superadmin"] },
{
title: "Custom Data",
path: "/cipp/custom-data/directory-extensions",
- roles: ["admin", "superadmin"]
+ roles: ["admin", "superadmin"],
},
{
title: "Advanced",
diff --git a/src/layouts/index.js b/src/layouts/index.js
index ddd1b2460645..6603b874f2e5 100644
--- a/src/layouts/index.js
+++ b/src/layouts/index.js
@@ -82,12 +82,17 @@ export const Layout = (props) => {
const [menuItems, setMenuItems] = useState(nativeMenuItems);
const currentTenant = settings?.currentTenant;
const currentRole = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
+ });
+ const [hideSidebar, setHideSidebar] = useState(false);
+
+ const swaStatus = ApiGetCall({
+ url: "/.auth/me",
+ queryKey: "authmeswa",
staleTime: 120000,
refetchOnWindowFocus: true,
});
- const [hideSidebar, setHideSidebar] = useState(false);
useEffect(() => {
if (currentRole.isSuccess && !currentRole.isFetching) {
@@ -118,8 +123,15 @@ export const Layout = (props) => {
const filteredMenu = filterItemsByRole(nativeMenuItems);
setMenuItems(filteredMenu);
+ } else if (
+ swaStatus.isLoading ||
+ swaStatus.data?.clientPrincipal === null ||
+ swaStatus.data === undefined ||
+ currentRole.isLoading
+ ) {
+ setHideSidebar(true);
}
- }, [currentRole.isSuccess]);
+ }, [currentRole.isSuccess, swaStatus.data, swaStatus.isLoading]);
const handleNavPin = useCallback(() => {
settings.handleUpdate({
@@ -181,11 +193,11 @@ export const Layout = (props) => {
});
useEffect(() => {
- if (version.isFetched && !alertsAPI.isFetched) {
+ if (!hideSidebar && version.isFetched && !alertsAPI.isFetched) {
alertsAPI.waiting = true;
alertsAPI.refetch();
}
- }, [version, alertsAPI]);
+ }, [version, alertsAPI, hideSidebar]);
useEffect(() => {
if (alertsAPI.isSuccess && !alertsAPI.isFetching) {
@@ -238,6 +250,27 @@ export const Layout = (props) => {
}}
>
+
+ {!setupCompleted && (
+
+
+
+ Setup has not been completed.
+
+
+
+
+ )}
{(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? (
@@ -255,30 +288,7 @@ export const Layout = (props) => {
) : (
- <>
-
- {!setupCompleted && (
-
-
-
- Setup has not been completed.
-
-
-
-
- )}
- {children}
- >
+ <>{children}>
)}
diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js
index ca6d32b13ed8..7f58bb1d5e95 100644
--- a/src/layouts/side-nav.js
+++ b/src/layouts/side-nav.js
@@ -15,7 +15,6 @@ const markOpenItems = (items, pathname) => {
return items.map((item) => {
const checkPath = !!(item.path && pathname);
const exactMatch = checkPath ? pathname === item.path : false;
- // Use startsWith for partial matches so that subpages not in the menu still keep parent open
const partialMatch = checkPath ? pathname.startsWith(item.path) : false;
let openImmediately = exactMatch;
@@ -24,11 +23,9 @@ const markOpenItems = (items, pathname) => {
if (newItems.length > 0) {
newItems = markOpenItems(newItems, pathname);
const childOpen = newItems.some((child) => child.openImmediately);
- // Parent should open if exactMatch, childOpen, or partialMatch
- openImmediately = openImmediately || childOpen || partialMatch;
+ openImmediately = openImmediately || childOpen || exactMatch; // Ensure parent opens if child is open
} else {
- // For leaf items, consider them open if exact or partial match
- openImmediately = openImmediately || partialMatch;
+ openImmediately = openImmediately || partialMatch; // Leaf items open on partial match
}
return {
@@ -47,8 +44,6 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname }) => {
const exactMatch = checkPath && pathname === item.path;
const partialMatch = checkPath && pathname.startsWith(item.path);
- // Consider item active if exactMatch or partialMatch for leaf items
- // For parent items, being active is determined by their children or openImmediately
const hasChildren = item.items && item.items.length > 0;
const isActive = exactMatch || (partialMatch && !hasChildren);
@@ -107,7 +102,7 @@ export const SideNav = (props) => {
const pathname = usePathname();
const [hovered, setHovered] = useState(false);
const collapse = !(pinned || hovered);
- const { data: profile } = ApiGetCall({ url: "/.auth/me", queryKey: "authmecipp" });
+ const { data: profile } = ApiGetCall({ url: "/api/me", queryKey: "authmecipp" });
// Preprocess items to mark which should be open
const processedItems = markOpenItems(items, pathname);
@@ -159,90 +154,96 @@ export const SideNav = (props) => {
const randomimg = randomSponsorImage();
return (
- {
- setHovered(true);
- },
- onMouseLeave: () => {
- setHovered(false);
- },
- sx: {
- backgroundColor: "background.default",
- height: `calc(100% - ${TOP_NAV_HEIGHT}px)`,
- overflowX: "hidden",
- top: TOP_NAV_HEIGHT,
- transition: "width 250ms ease-in-out",
- width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH,
- zIndex: (theme) => theme.zIndex.appBar - 100,
- },
- }}
- >
-
-
+ {profile?.clientPrincipal && profile?.clientPrincipal?.userRoles?.length > 2 && (
+ {
+ setHovered(true);
+ },
+ onMouseLeave: () => {
+ setHovered(false);
+ },
+ sx: {
+ backgroundColor: "background.default",
+ height: `calc(100% - ${TOP_NAV_HEIGHT}px)`,
+ overflowX: "hidden",
+ top: TOP_NAV_HEIGHT,
+ transition: "width 250ms ease-in-out",
+ width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH,
+ zIndex: (theme) => theme.zIndex.appBar - 100,
+ },
}}
>
-
- {renderItems({
- collapse,
- depth: 0,
- items: processedItems,
- pathname,
- })}
-
- {profile?.clientPrincipal && (
- <>
-
-
- This application is sponsored by
-
+
-
window.open(randomimg.link)}
- width={"100px"}
- />
-
- >
- )}
-
-
-
+ {renderItems({
+ collapse,
+ depth: 0,
+ items: processedItems,
+ pathname,
+ })}
+ {" "}
+ {/* Add this closing tag */}
+ {profile?.clientPrincipal && (
+ <>
+
+
+ This application is sponsored by
+
+
+
window.open(randomimg.link)}
+ width={"100px"}
+ />
+
+ >
+ )}
+ {" "}
+ {/* Closing tag for the parent Box */}
+
+
+ )}
+ >
);
};
diff --git a/src/pages/401.js b/src/pages/401.js
index ccb828549c51..0787504e2319 100644
--- a/src/pages/401.js
+++ b/src/pages/401.js
@@ -1,4 +1,5 @@
-import { Box, Container, Grid, Stack } from "@mui/material";
+import { Box, Container, Stack } from "@mui/material";
+import { Grid } from "@mui/system";
import Head from "next/head";
import { CippImageCard } from "../components/CippCards/CippImageCard.jsx";
import { Layout as DashboardLayout } from "../layouts/index.js";
@@ -25,7 +26,7 @@ const Page = () => (
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+
(
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+
{
alignItems="center"
sx={{ height: "100%" }}
>
-
+
- import("@tanstack/react-query-devtools/build/modern/production.js").then((d) => ({
- default: d.ReactQueryDevtools,
- }))
-);
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
const clientSideEmotionCache = createEmotionCache();
@@ -49,6 +46,43 @@ const App = (props) => {
const pathname = usePathname();
const route = useRouter();
+ const excludeQueryKeys = ["authmeswa"];
+
+ // 👇 Persist TanStack Query cache to localStorage
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ const localStoragePersister = createSyncStoragePersister({
+ storage: window.localStorage,
+ });
+
+ persistQueryClient({
+ queryClient,
+ persister: localStoragePersister,
+ maxAge: 1000 * 60 * 60 * 24, // 24 hours
+ staleTime: 1000 * 60 * 5, // optional: 5 minutes
+ buster: "v1",
+ dehydrateOptions: {
+ shouldDehydrateQuery: (query) => {
+ const queryIsReadyForPersistence = query.state.status === "success";
+ if (queryIsReadyForPersistence) {
+ const { queryKey } = query;
+ // Check if queryKey exists and has elements before accessing index 0
+ if (!queryKey || !queryKey.length) {
+ return false;
+ }
+ const queryKeyString = String(queryKey[0] || '');
+ const excludeFromPersisting = excludeQueryKeys.some((key) =>
+ queryKeyString.includes(key)
+ );
+ return !excludeFromPersisting;
+ }
+ return queryIsReadyForPersistence;
+ },
+ },
+ });
+ }
+ }, []);
+
const speedDialActions = [
{
id: "license",
@@ -144,7 +178,7 @@ const App = (props) => {
{settings.isInitialized && settings?.showDevtools === true ? (
-
+
) : null}
>
diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js
new file mode 100644
index 000000000000..c15013faa569
--- /dev/null
+++ b/src/pages/authredirect.js
@@ -0,0 +1,47 @@
+import { Box, Container, Stack } from "@mui/material";
+import { Grid } from "@mui/system";
+import Head from "next/head";
+import { CippImageCard } from "../components/CippCards/CippImageCard.jsx";
+import { Layout as DashboardLayout } from "../layouts/index.js";
+
+const Page = () => (
+ <>
+
+
+ Authentication complete
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export default Page;
diff --git a/src/pages/cipp/advanced/exchange-cmdlets.js b/src/pages/cipp/advanced/exchange-cmdlets.js
index 76de317c7df1..0a902d652aeb 100644
--- a/src/pages/cipp/advanced/exchange-cmdlets.js
+++ b/src/pages/cipp/advanced/exchange-cmdlets.js
@@ -3,13 +3,13 @@ import {
Button,
Container,
Stack,
- Grid,
Dialog,
DialogTitle,
DialogContent,
IconButton,
Skeleton,
} from "@mui/material";
+import { Grid } from "@mui/system";
import { useForm } from "react-hook-form";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiPostCall } from "/src/api/ApiCall";
@@ -99,11 +99,11 @@ const Page = () => {
{/* Tenant Filter */}
-
+
{/* Compliance Filter */}
-
+
{
/>
{/* AsApp Filter */}
-
+
{
/>
{/* Submit Button */}
-
+
}>
Search
diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js
index 141bebd8011d..ee5409aa1a1c 100644
--- a/src/pages/cipp/integrations/index.js
+++ b/src/pages/cipp/integrations/index.js
@@ -7,7 +7,6 @@ import {
CardActions,
CardContent,
Container,
- Grid,
Skeleton,
Stack,
Typography,
@@ -17,6 +16,7 @@ import { Sync } from "@mui/icons-material";
import { useSettings } from "/src/hooks/use-settings";
import { ApiGetCall } from "/src/api/ApiCall";
import Link from "next/link";
+import { Grid } from "@mui/system";
const Page = () => {
const settings = useSettings();
@@ -67,7 +67,7 @@ const Page = () => {
}
return (
-
+
{
const formControl = useForm({
defaultValues: {
startDate: null,
- toggleSwitch: false,
+ endDate: null,
+ username: "",
+ severity: [],
},
});
const [expanded, setExpanded] = useState(false); // State for Accordion
const [filterEnabled, setFilterEnabled] = useState(false); // State for filter toggle
- const [dateFilter, setDateFilter] = useState(null); // State for date filter
+ const [startDate, setStartDate] = useState(null); // State for start date filter
+ const [endDate, setEndDate] = useState(null); // State for end date filter
+ const [username, setUsername] = useState(null); // State for username filter
+ const [severity, setSeverity] = useState(null); // State for severity filter
+
+ // Watch date fields to show warning for large date ranges
+ const watchStartDate = formControl.watch("startDate");
+ const watchEndDate = formControl.watch("endDate");
+
+ // Component to display warning for large date ranges
+ const DateRangeWarning = () => {
+ if (!watchStartDate || !watchEndDate) return null;
+
+ const startDateMs = new Date(watchStartDate * 1000);
+ const endDateMs = new Date(watchEndDate * 1000);
+ const daysDifference = (endDateMs - startDateMs) / (1000 * 60 * 60 * 24);
+
+ if (daysDifference > 10) {
+ return (
+
+
+ You have selected a date range of {Math.ceil(daysDifference)} days. Large date ranges
+ may cause timeouts or errors due to the amount of data being processed. Consider
+ narrowing your date range if you encounter issues.
+
+
+ );
+ }
+
+ return null;
+ };
const onSubmit = (data) => {
- // Set filter states based on form submission
- setFilterEnabled(data.toggleSwitch);
- setDateFilter(
+ // Check if any filter is applied
+ const hasFilter =
+ data.startDate !== null ||
+ data.endDate !== null ||
+ data.username !== null ||
+ data.severity?.length > 0;
+ setFilterEnabled(hasFilter);
+
+ // Format start date if available
+ setStartDate(
data.startDate
? new Date(data.startDate * 1000).toISOString().split("T")[0].replace(/-/g, "")
: null
);
+
+ // Format end date if available
+ setEndDate(
+ data.endDate
+ ? new Date(data.endDate * 1000).toISOString().split("T")[0].replace(/-/g, "")
+ : null
+ );
+
+ // Set username filter if available
+ setUsername(data.username || null);
+
+ // Set severity filter if available (convert array to comma-separated string)
+ setSeverity(
+ data.severity && data.severity.length > 0
+ ? data.severity.map((item) => item.value).join(",")
+ : null
+ );
+
+ // Close the accordion after applying filters
+ setExpanded(false);
+ };
+
+ const clearFilters = () => {
+ formControl.reset({
+ startDate: null,
+ endDate: null,
+ username: "",
+ severity: [],
+ });
+ setFilterEnabled(false);
+ setStartDate(null);
+ setEndDate(null);
+ setUsername(null);
+ setSeverity(null);
+ setExpanded(false); // Close the accordion when clearing filters
};
return (
@@ -56,37 +135,156 @@ const Page = () => {
tableFilter={
setExpanded(!expanded)}>
}>
- Logbook Filters
+
+
+
+
+
+ Logbook Filters
+ {filterEnabled ? (
+
+ (
+ {startDate || endDate ? (
+ <>
+ {startDate
+ ? new Date(
+ startDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00"
+ ).toLocaleDateString()
+ : new Date().toLocaleDateString()}
+ {startDate && endDate ? " - " : ""}
+ {endDate
+ ? new Date(
+ endDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00"
+ ).toLocaleDateString()
+ : ""}
+ >
+ ) : null}
+ {username && (startDate || endDate) && " | "}
+ {username && <>User: {username}>}
+ {severity && (username || startDate || endDate) && " | "}
+ {severity && <>Severity: {severity.replace(/,/g, ", ")}>})
+
+ ) : (
+
+ (Today: {new Date().toLocaleDateString()})
+
+ )}
+
+
@@ -96,10 +294,13 @@ const Page = () => {
title={pageTitle}
apiUrl={apiUrl}
simpleColumns={simpleColumns}
- queryKey={`Listlogs-${dateFilter}-${filterEnabled}`}
+ queryKey={`Listlogs-${startDate}-${endDate}-${username}-${severity}-${filterEnabled}`}
tenantInTitle={false}
apiData={{
- DateFilter: dateFilter, // Pass date filter from state
+ StartDate: startDate, // Pass start date filter from state
+ EndDate: endDate, // Pass end date filter from state
+ User: username, // Pass username filter from state
+ Severity: severity, // Pass severity filter from state
Filter: filterEnabled, // Pass filter toggle state
}}
/>
@@ -108,9 +309,16 @@ const Page = () => {
/* Comment to Developer:
- The filter is inside an expandable Accordion. By default, the filter is collapsed.
- - The "Apply Filters" button sets the form data for date and filter toggle.
+ - The "Apply Filters" button sets the form data for date, username, and severity filters.
+ - The "Clear Filters" button resets all filters and disables filtering.
+ - Filters are automatically enabled when any filter parameter is set.
- Form state is managed using react-hook-form, and the filter states are applied to the table.
- - The DateFilter is passed in 'YYYYMMDD' format, and Filter toggle is passed as a boolean.
+ - Both StartDate and EndDate are passed to the API in 'YYYYMMDD' format.
+ - The User parameter is passed directly as a string for username filtering.
+ - The Severity parameter is passed as a comma-separated list of severity levels.
+ - The Filter toggle is passed as a boolean and is automatically enabled when any filter is set.
+ - A warning alert is displayed when the selected date range exceeds 10 days instead of enforcing
+ a strict limit. This helps users understand potential issues with large data sets.
*/
Page.getLayout = (page) => {page};
diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js
index 40ccac234133..07c063615a0b 100644
--- a/src/pages/cipp/preferences.js
+++ b/src/pages/cipp/preferences.js
@@ -1,6 +1,6 @@
import Head from "next/head";
import { Box, Container, Stack } from "@mui/material";
-import Grid from "@mui/material/Grid2";
+import { Grid } from "@mui/system";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard";
import CippFormComponent from "../../components/CippComponents/CippFormComponent";
@@ -17,10 +17,8 @@ const Page = () => {
const formcontrol = useForm({ mode: "onChange", defaultValues: settings });
const auth = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
const addedAttributes = [
@@ -92,7 +90,6 @@ const Page = () => {
value: (
{
{
{backendInfo.map((item) => (
-
+
))}
diff --git a/src/pages/cipp/settings/backup.js b/src/pages/cipp/settings/backup.js
index 6935872881c2..40e8482ec070 100644
--- a/src/pages/cipp/settings/backup.js
+++ b/src/pages/cipp/settings/backup.js
@@ -1,4 +1,5 @@
-import { Box, Button, CardContent, Grid, Stack, Typography, Skeleton } from "@mui/material";
+import { Box, Button, CardContent, Stack, Typography, Skeleton } from "@mui/material";
+import { Grid } from "@mui/system";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import CippPageCard from "../../../components/CippCards/CippPageCard";
@@ -55,7 +56,7 @@ const Page = () => {
});
const NextBackupRun = (props) => {
- const date = new Date(props.date * 1000);
+ const date = new Date(props.date);
if (isNaN(date)) {
return "Not Scheduled";
} else {
@@ -135,6 +136,7 @@ const Page = () => {
confirmText: "Are you sure you want to restore this backup?",
relatedQueryKeys: ["BackupList"],
multiPost: false,
+ hideBulk: true,
},
{
label: "Download Backup",
diff --git a/src/pages/cipp/settings/index.js b/src/pages/cipp/settings/index.js
index 430a436fd51e..9aafb0e5523f 100644
--- a/src/pages/cipp/settings/index.js
+++ b/src/pages/cipp/settings/index.js
@@ -1,4 +1,5 @@
-import { Container, Grid } from "@mui/material";
+import { Container } from "@mui/material";
+import { Grid } from "@mui/system";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import tabOptions from "./tabOptions";
@@ -12,19 +13,19 @@ const Page = () => {
return (
-
+
-
+
-
+
-
+
-
+
diff --git a/src/pages/cipp/settings/notifications.js b/src/pages/cipp/settings/notifications.js
index b9fc1cd25aa6..a65eb064a49c 100644
--- a/src/pages/cipp/settings/notifications.js
+++ b/src/pages/cipp/settings/notifications.js
@@ -3,75 +3,18 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import tabOptions from "./tabOptions";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
import { useForm } from "react-hook-form";
-import CippFormComponent from "../../../components/CippComponents/CippFormComponent";
-import { Box, Button, Grid } from "@mui/material";
-import { ApiGetCall } from "../../../api/ApiCall";
-import { useEffect } from "react";
+import { Button } from "@mui/material";
import { useDialog } from "../../../hooks/use-dialog";
-import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog";
+import { CippNotificationForm } from "../../../components/CippComponents/CippNotificationForm";
const Page = () => {
const pageTitle = "Notification Settings";
const notificationDialog = useDialog();
- const listNotificationConfig = ApiGetCall({
- url: "/api/ListNotificationConfig",
- queryKey: "ListNotificationConfig",
- });
-
- const logTypes = [
- { label: "Updates Status", value: "Updates" },
- { label: "All Standards", value: "Standards" },
- { label: "Token Events", value: "TokensUpdater" },
- { label: "Changing DNS Settings", value: "ExecDnsConfig" },
- { label: "Adding excluded licenses", value: "ExecExcludeLicenses" },
- { label: "Adding excluded tenants", value: "ExecExcludeTenant" },
- { label: "Editing a user", value: "EditUser" },
- { label: "Adding or deploying applications", value: "ChocoApp" },
- { label: "Adding autopilot devices", value: "AddAPDevice" },
- { label: "Editing a tenant", value: "EditTenant" },
- { label: "Adding an MSP app", value: "AddMSPApp" },
- { label: "Adding a user", value: "AddUser" },
- { label: "Adding a group", value: "AddGroup" },
- { label: "Adding a tenant", value: "NewTenant" },
- { label: "Executing the offboard wizard", value: "ExecOffboardUser" },
- ];
- const severityTypes = [
- { label: "Alert", value: "Alert" },
- { label: "Error", value: "Error" },
- { label: "Info", value: "Info" },
- { label: "Warning", value: "Warning" },
- { label: "Critical", value: "Critical" },
- ];
-
const formControl = useForm({
mode: "onChange",
});
- useEffect(() => {
- if (listNotificationConfig.isSuccess) {
- var logsToInclude = [];
- listNotificationConfig.data?.logsToInclude.map((log) => {
- var logType = logTypes.find((logType) => logType.value === log);
- if (logType) {
- logsToInclude.push(logType);
- }
- });
-
- formControl.reset({
- email: listNotificationConfig.data?.email,
- webhook: listNotificationConfig.data?.webhook,
- logsToInclude: logsToInclude,
- Severity: listNotificationConfig.data?.Severity.map((severity) => {
- return severityTypes.find((severityType) => severityType.value === severity);
- }),
- onePerTenant: listNotificationConfig.data?.onePerTenant,
- sendtoIntegration: listNotificationConfig.data?.sendtoIntegration,
- includeTenantId: listNotificationConfig.data?.includeTenantId,
- });
- }
- }, [listNotificationConfig.isSuccess]);
-
return (
{
resetForm={false}
postUrl="/api/ExecNotificationConfig"
relatedQueryKeys={["ListNotificationConfig"]}
- isFetching={listNotificationConfig.isFetching}
- addedButtons={
-
- }
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
);
diff --git a/src/pages/cipp/settings/partner-webhooks.js b/src/pages/cipp/settings/partner-webhooks.js
index 2a5c38ddae47..0cc6adeb8a53 100644
--- a/src/pages/cipp/settings/partner-webhooks.js
+++ b/src/pages/cipp/settings/partner-webhooks.js
@@ -8,7 +8,6 @@ import {
Button,
Card,
Chip,
- Grid,
Stack,
Typography,
Link,
@@ -17,6 +16,7 @@ import {
IconButton,
SvgIcon,
} from "@mui/material";
+import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall";
import { useEffect } from "react";
@@ -132,7 +132,7 @@ const Page = () => {
}
>
-
+
Subscribe to Microsoft Partner center webhooks to enable automatic tenant onboarding and
alerting. Updating the settings will replace any existing webhook subscription with one
@@ -147,7 +147,7 @@ const Page = () => {
for more information on the webhook types.
-
+
{
showDivider={false}
/>
-
+
{
formControl={formControl}
/>
-
+
{
/>
{testRunning && (
-
+
{
return (
-
+
-
+
-
+
-
+
diff --git a/src/pages/cipp/settings/tenants.js b/src/pages/cipp/settings/tenants.js
index 320e22fa5add..a5495b4b77e8 100644
--- a/src/pages/cipp/settings/tenants.js
+++ b/src/pages/cipp/settings/tenants.js
@@ -1,148 +1,12 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
-import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import tabOptions from "./tabOptions";
-import { Button, SvgIcon } from "@mui/material";
-import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog";
-import { useDialog } from "/src/hooks/use-dialog";
-import { Sync, Block, PlayArrow, RestartAlt, Delete, Add } from "@mui/icons-material";
+import { CippTenantTable } from "../../../components/CippWizard/CippTenantTable";
const Page = () => {
const pageTitle = "Tenants - Backend";
- const createDialog = useDialog();
- // Actions formatted as per your guidelines
- const actions = [
- {
- label: "Exclude Tenants",
- type: "POST",
- url: `/api/ExecExcludeTenant?AddExclusion=true`,
- icon: ,
- data: { value: "customerId" },
- confirmText: "Are you sure you want to exclude these tenants?",
- multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
- },
- {
- label: "Include Tenants",
- type: "POST",
- url: `/api/ExecExcludeTenant?RemoveExclusion=true`,
- icon: ,
- data: { value: "customerId" },
- confirmText: "Are you sure you want to include these tenants?",
- multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
- },
- {
- label: "Refresh CPV Permissions",
- type: "POST",
- url: `/api/ExecCPVPermissions`,
- icon: ,
- data: { tenantFilter: "customerId" },
- confirmText: "Are you sure you want to refresh the CPV permissions for these tenants?",
- multiPost: false,
- },
- {
- label: "Reset CPV Permissions",
- type: "POST",
- url: `/api/ExecCPVPermissions?&ResetSP=true`,
- icon: ,
- data: { tenantFilter: "customerId" },
- confirmText:
- "Are you sure you want to reset the CPV permissions for these tenants? (This will delete the Service Principal and re-add it.)",
- multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
- },
- {
- label: "Remove Tenant",
- type: "POST",
- url: `/api/ExecRemoveTenant`,
- icon: ,
- data: { TenantID: "customerId" },
- confirmText: "Are you sure you want to remove this tenant?",
- multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
- },
- ];
-
- // Offcanvas details
- const offCanvas = {
- extendedInfoFields: [
- "displayName",
- "defaultDomainName",
- "Excluded",
- "ExcludeDate",
- "ExcludeUser",
- ],
- actions: actions,
- };
-
- // Columns for the table
- const columns = [
- "displayName", // Tenant Name
- "defaultDomainName", // Default Domain
- "Excluded", // Excluded Status
- "ExcludeDate", // Exclude Date
- "ExcludeUser", // Exclude User
- ];
-
- return (
- <>
-
-
-
-
- Force Refresh
-
- }
- tenantInTitle={false}
- apiUrl="/api/ExecExcludeTenant?ListAll=True"
- actions={actions}
- offCanvas={offCanvas}
- simpleColumns={columns}
- filters={[
- {
- filterName: "Included tenants",
- //true or false filters by yes/no
- value: [{ id: "Excluded", value: "No" }],
- type: "column",
- },
- {
- filterName: "Excluded tenants",
- value: [{ id: "Excluded", value: "Yes" }],
- type: "column",
- },
- ]}
- />
-
- >
- );
+ return ;
};
Page.getLayout = (page) => (
diff --git a/src/pages/cipp/super-admin/cipp-roles/add.js b/src/pages/cipp/super-admin/cipp-roles/add.js
new file mode 100644
index 000000000000..1734bd047245
--- /dev/null
+++ b/src/pages/cipp/super-admin/cipp-roles/add.js
@@ -0,0 +1,23 @@
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { CippRoleAddEdit } from "/src/components/CippSettings/CippRoleAddEdit";
+import { CardContent, Stack, Alert } from "@mui/material";
+
+const AddRolePage = () => {
+ return (
+
+
+
+
+ Create a new custom role with specific permissions and settings.
+
+
+
+
+
+ );
+};
+
+AddRolePage.getLayout = (page) => {page};
+
+export default AddRolePage;
diff --git a/src/pages/cipp/super-admin/cipp-roles/edit.js b/src/pages/cipp/super-admin/cipp-roles/edit.js
new file mode 100644
index 000000000000..85a4b2e0c431
--- /dev/null
+++ b/src/pages/cipp/super-admin/cipp-roles/edit.js
@@ -0,0 +1,28 @@
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { CippRoleAddEdit } from "/src/components/CippSettings/CippRoleAddEdit";
+import { CardContent, Stack, Alert } from "@mui/material";
+
+const EditRolePage = () => {
+ const router = useRouter();
+ const { role } = router.query;
+
+ return (
+
+
+
+
+ Editing an existing role will update the permissions for all users assigned to this
+ role.
+
+
+
+
+
+ );
+};
+
+EditRolePage.getLayout = (page) => {page};
+
+export default EditRolePage;
diff --git a/src/pages/cipp/super-admin/custom-roles.js b/src/pages/cipp/super-admin/cipp-roles/index.js
similarity index 79%
rename from src/pages/cipp/super-admin/custom-roles.js
rename to src/pages/cipp/super-admin/cipp-roles/index.js
index 76e95c16241b..9f2ea69fbfa5 100644
--- a/src/pages/cipp/super-admin/custom-roles.js
+++ b/src/pages/cipp/super-admin/cipp-roles/index.js
@@ -1,18 +1,18 @@
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import tabOptions from "./tabOptions";
+import tabOptions from "../tabOptions";
import CippPageCard from "/src/components/CippCards/CippPageCard";
-import { CippCustomRoles } from "/src/components/CippSettings/CippCustomRoles";
+import CippRoles from "/src/components/CippSettings/CippRoles";
import { Alert, CardContent, Stack, Typography } from "@mui/material";
import { WarningAmberOutlined } from "@mui/icons-material";
const Page = () => {
return (
-
+
- Custom roles can be used to restrict permissions for users with the 'editor' or
+ CIPP roles can be used to restrict permissions for users with the 'editor' or
'readonly' roles in CIPP. They can be limited to a subset of tenants and API
permissions. To restrict direct API access, create a role with the name 'CIPP-API'.
@@ -20,7 +20,7 @@ const Page = () => {
This functionality is in beta and should be treated as such. The custom role must be
added to the user in SWA in conjunction with the base role. (e.g. editor,mycustomrole)
-
+
diff --git a/src/pages/cipp/super-admin/function-offloading.js b/src/pages/cipp/super-admin/function-offloading.js
index 642c9f48a411..ed94497c8047 100644
--- a/src/pages/cipp/super-admin/function-offloading.js
+++ b/src/pages/cipp/super-admin/function-offloading.js
@@ -4,7 +4,7 @@ import tabOptions from "./tabOptions";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
import { useForm } from "react-hook-form";
import { Alert, Typography, Link } from "@mui/material";
-import Grid from "@mui/material/Grid2";
+import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall";
import { useEffect } from "react";
@@ -28,7 +28,7 @@ const Page = () => {
});
const deleteOffloadEntry = ApiPostCall({
- urlfromdata: true,
+ urlFromData: true,
relatedQueryKeys: ["execOffloadFunctions"],
});
diff --git a/src/pages/cipp/super-admin/tabOptions.json b/src/pages/cipp/super-admin/tabOptions.json
index 9c1e7af7eb16..8697d8c2c5e2 100644
--- a/src/pages/cipp/super-admin/tabOptions.json
+++ b/src/pages/cipp/super-admin/tabOptions.json
@@ -8,8 +8,8 @@
"path": "/cipp/super-admin/function-offloading"
},
{
- "label": "Custom Roles",
- "path": "/cipp/super-admin/custom-roles"
+ "label": "CIPP Roles",
+ "path": "/cipp/super-admin/cipp-roles"
},
{
"label": "SAM App Roles",
diff --git a/src/pages/cipp/super-admin/tenant-mode.js b/src/pages/cipp/super-admin/tenant-mode.js
index 45b1872e9444..61b2da589794 100644
--- a/src/pages/cipp/super-admin/tenant-mode.js
+++ b/src/pages/cipp/super-admin/tenant-mode.js
@@ -3,7 +3,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import tabOptions from "./tabOptions";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
import { useForm } from "react-hook-form";
-import { Grid, Typography } from "@mui/material";
+import { Typography } from "@mui/material";
+import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall } from "../../../api/ApiCall";
import { useEffect } from "react";
@@ -58,7 +59,7 @@ const Page = () => {
queryKey={["execPartnerMode", "TenantSelector"]}
>
-
+
The configuration settings below should only be modified by a super admin. Super admins
can configure what tenant mode CIPP operates in. See{" "}
@@ -72,7 +73,7 @@ const Page = () => {
for more information on how to configure these modes and what they mean.
-
+
{
title="Add Contact"
backButtonTitle="Contacts Overview"
postUrl="/api/AddContact"
+ resetForm={true}
customDataformatter={(values) => {
// Add tenantDomain to the payload
return {
@@ -42,7 +44,7 @@ const AddContact = () => {
>
{/* Display Name */}
-
+
{
{/* First Name and Last Name */}
-
+
{
formControl={formControl}
/>
-
+
{
{/* Email */}
-
+
{
{/* Hide from GAL */}
-
+
{
>
{/* Display Name */}
-
+
{
{/* First Name and Last Name */}
-
+
{
formControl={formControl}
/>
-
+
{
{/* Email */}
-
+
{
{/* Hide from GAL */}
-
+
{
{/* Company Information */}
-
+
{
formControl={formControl}
/>
-
+
{
{/* Address Information */}
-
+
{
formControl={formControl}
/>
-
+
-
+
{
formControl={formControl}
/>
-
+
{
{/* Phone Numbers */}
-
+
{
formControl={formControl}
/>
-
+
{
}}
>
-
+
{
{/* Email */}
-
+
{
formControl={formControl}
/>
-
+
diff --git a/src/pages/email/administration/tenant-allow-block-lists/add.jsx b/src/pages/email/administration/tenant-allow-block-lists/add.jsx
index 434e4cbaa7fa..1fecb155bd9f 100644
--- a/src/pages/email/administration/tenant-allow-block-lists/add.jsx
+++ b/src/pages/email/administration/tenant-allow-block-lists/add.jsx
@@ -1,10 +1,12 @@
-import React from "react";
-import { Grid } from "@mui/material";
-import { useForm } from "react-hook-form";
+import { useEffect } from "react";
+import "@mui/material";
+import { Grid } from "@mui/system";
+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 "../../../../hooks/use-settings";
+import { getCippValidator } from "/src/utils/get-cipp-validator";
const AddTenantAllowBlockList = () => {
const tenantDomain = useSettings().currentTenant;
@@ -17,9 +19,146 @@ const AddTenantAllowBlockList = () => {
listType: null,
listMethod: null,
NoExpiration: false,
+ RemoveAfter: false,
},
});
+ const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" });
+ const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" });
+ const listMethod = useWatch({ control: formControl.control, name: "listMethod" });
+ const listType = useWatch({ control: formControl.control, name: "listType" });
+
+ const isListMethodBlock = listMethod?.value === "Block";
+ const isListTypeFileHash = listType?.value === "FileHash";
+ const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value);
+ const isNoExpirationCompatible = isListMethodBlock ||
+ (listMethod?.value === "Allow" && (listType?.value === "Url" || listType?.value === "IP"));
+
+ useEffect(() => {
+ if (noExpiration) {
+ formControl.setValue("RemoveAfter", false);
+ }
+
+ if (removeAfter) {
+ formControl.setValue("NoExpiration", false);
+ }
+
+ if (isListMethodBlock) {
+ formControl.setValue("RemoveAfter", false);
+ }
+
+ if (listType && !isListTypeSenderUrlOrFileHash) {
+ formControl.setValue("RemoveAfter", false);
+ }
+
+ if (isListTypeFileHash) {
+ formControl.setValue("listMethod", { label: "Block", value: "Block" });
+ }
+
+ if (listMethod || listType) {
+ if (!isNoExpirationCompatible && noExpiration) {
+ formControl.setValue("NoExpiration", false);
+ }
+ }
+ }, [
+ noExpiration,
+ removeAfter,
+ isListMethodBlock,
+ listType,
+ isListTypeSenderUrlOrFileHash,
+ isListTypeFileHash,
+ isNoExpirationCompatible,
+ formControl
+ ]);
+
+ const validateEntries = (value) => {
+ if (!value) return true;
+
+ const entries = value.split(/[,;]/).map(e => e.trim());
+ const currentListType = listType?.value;
+
+ if (currentListType === "FileHash") {
+ for (const entry of entries) {
+ if (entry.length !== 64)
+ return "File hash entries must be exactly 64 characters";
+
+ const hashResult = getCippValidator(entry, "sha256");
+ if (hashResult !== true)
+ return hashResult;
+ }
+ } else if (currentListType === "IP") {
+ for (const entry of entries) {
+ const ipv6Result = getCippValidator(entry, "ipv6");
+ const ipv6CidrResult = getCippValidator(entry, "ipv6cidr");
+
+ if (ipv6Result !== true && ipv6CidrResult !== true)
+ return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation";
+ }
+ } else if (currentListType === "Url") {
+ for (const entry of entries) {
+ if (entry.length > 250)
+ return "URL entries must be 250 characters or less";
+
+ // For entries with wildcards, use the improved wildcard validators
+ if (entry.includes('*') || entry.includes('~')) {
+ // Try both wildcard validators
+ const wildcardUrlResult = getCippValidator(entry, "wildcardUrl");
+ const wildcardDomainResult = getCippValidator(entry, "wildcardDomain");
+
+ if (wildcardUrlResult !== true && wildcardDomainResult !== true) {
+ // If basic pattern check fails too, give a more specific message
+ if (!/^[a-zA-Z0-9\.\-\*\~\/]+$/.test(entry)) {
+ return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)";
+ }
+
+ // If it has basic valid characters but doesn't match our patterns
+ return "Invalid wildcard format. Common formats are *.domain.com or domain.*";
+ }
+ continue;
+ }
+
+ // For non-wildcard entries, use standard validators
+ const ipv4Result = getCippValidator(entry, "ip");
+ const ipv4CidrResult = getCippValidator(entry, "ipv4cidr");
+ const ipv6Result = getCippValidator(entry, "ipv6");
+ const ipv6CidrResult = getCippValidator(entry, "ipv6cidr");
+ const hostnameResult = getCippValidator(entry, "hostname");
+ const urlResult = getCippValidator(entry, "url");
+
+ // If none of the validators pass
+ if (ipv4Result !== true &&
+ ipv4CidrResult !== true &&
+ ipv6Result !== true &&
+ ipv6CidrResult !== true &&
+ hostnameResult !== true &&
+ urlResult !== true) {
+ return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses";
+ }
+ }
+ } else if (currentListType === "Sender") {
+ for (const entry of entries) {
+ // Check for wildcards first
+ if (entry.includes('*') || entry.includes('~')) {
+ const wildcardDomainResult = getCippValidator(entry, "wildcardDomain");
+
+ if (wildcardDomainResult !== true) {
+ return "Invalid sender wildcard pattern. Common format is *.domain.com";
+ }
+ continue;
+ }
+
+ // For non-wildcard entries, use senderEntry validator
+ const senderResult = getCippValidator(entry, "senderEntry");
+
+ if (senderResult !== true) {
+ return senderResult;
+ }
+ }
+ }
+
+ return true;
+ };
+
return (
{
notes: values.notes,
listMethod: values.listMethod?.value,
NoExpiration: values.NoExpiration,
+ RemoveAfter: values.RemoveAfter
};
}}
>
{/* Entries */}
-
+
{/* Notes & List Type */}
-
+
{
formControl={formControl}
/>
-
+
{
creatable={false}
options={[
{ label: "Sender", value: "Sender" },
- { label: "Url", value: "Url" },
+ { label: "Url/IPv4", value: "Url" },
{ label: "FileHash", value: "FileHash" },
+ { label: "IPv6", value: "IP" },
]}
validators={{ required: "Please choose a List Type." }}
/>
{/* List Method */}
-
+
{
{ label: "Allow", value: "Allow" },
]}
validators={{ required: "Please select Block or Allow." }}
+ disabled={isListTypeFileHash}
+ helperText={
+ isListTypeFileHash
+ ? "FileHash entries can only be Blocked"
+ : "Choose whether to block or allow the entries"
+ }
/>
{/* No Expiration */}
-
+
+
+
+ {/* Remove After */}
+
+
diff --git a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js
index 4b05494fa344..dbd167e86105 100644
--- a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js
+++ b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js
@@ -2,15 +2,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import { Block } from "@mui/icons-material";
-/*
- NOTE for Devs:
- - The original component used a Redux selector (`useSelector`) for tenant data,
- which is handled by `CippTablePage` in the refactored version, thus eliminating `useSelector`.
- - The `ModalService` with `confirm` handling was originally used to confirm blocking sign-in.
- The action here replaces it with a confirmation text as per current guidelines.
- - Original button and `FontAwesomeIcon` (faBan) are not used since action confirmation is handled by CippTablePage.
-*/
-
const Page = () => {
return (
{
icon: ,
url: "/api/ExecDisableUser",
data: { ID: "id" },
- confirmText: "Are you sure you want to block the sign-in for this user?",
+ confirmText: "Are you sure you want to block the sign-in for this mailbox?",
+ condition: (row) => row.accountEnabled && !row.onPremisesSyncEnabled,
},
]}
offCanvas={{
@@ -31,6 +23,7 @@ const Page = () => {
"UserPrincipalName",
"displayName",
"accountEnabled",
+ "assignedLicenses",
"onPremisesSyncEnabled",
],
}}
@@ -38,8 +31,15 @@ const Page = () => {
"UserPrincipalName",
"displayName",
"accountEnabled",
+ "assignedLicenses",
"onPremisesSyncEnabled",
]}
+ filters={[
+ {
+ id: "accountEnabled",
+ value: "Yes"
+ }
+ ]}
/>
);
};
diff --git a/src/pages/email/resources/management/list-rooms/add.jsx b/src/pages/email/resources/management/list-rooms/add.jsx
index b1a7aeeec5e6..21aea3ae91d6 100644
--- a/src/pages/email/resources/management/list-rooms/add.jsx
+++ b/src/pages/email/resources/management/list-rooms/add.jsx
@@ -1,5 +1,6 @@
import React from "react";
-import { Grid, Divider } from "@mui/material";
+import { Divider } from "@mui/material";
+import { Grid } from "@mui/system";
import { useForm } from "react-hook-form";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
@@ -44,7 +45,7 @@ const AddRoomMailbox = () => {
>
{/* Display Name */}
-
+
{
{/* Username and Domain */}
-
+
{
validators={{ required: "Username is required" }}
/>
-
+
{
{/* Resource Capacity (Optional) */}
-
+
{
+ const timezones = `Azores Standard Time (UTC-01:00) Azores
+Cape Verde Standard Time (UTC-01:00) Cabo Verde Is.
+UTC-02 (UTC-02:00) Co-ordinated Universal Time-02
+Greenland Standard Time (UTC-02:00) Greenland
+Mid-Atlantic Standard Time (UTC-02:00) Mid-Atlantic - Old
+Tocantins Standard Time (UTC-03:00) Araguaina
+Paraguay Standard Time (UTC-03:00) Asuncion
+E. South America Standard Time (UTC-03:00) Brasilia
+SA Eastern Standard Time (UTC-03:00) Cayenne, Fortaleza
+Argentina Standard Time (UTC-03:00) City of Buenos Aires
+Montevideo Standard Time (UTC-03:00) Montevideo
+Magallanes Standard Time (UTC-03:00) Punta Arenas
+Saint Pierre Standard Time (UTC-03:00) Saint Pierre and Miquelon
+Bahia Standard Time (UTC-03:00) Salvador
+Newfoundland Standard Time (UTC-03:30) Newfoundland
+Atlantic Standard Time (UTC-04:00) Atlantic Time (Canada)
+Venezuela Standard Time (UTC-04:00) Caracas
+Central Brazilian Standard Time (UTC-04:00) Cuiaba
+SA Western Standard Time (UTC-04:00) Georgetown, La Paz, Manaus, San Juan
+Pacific SA Standard Time (UTC-04:00) Santiago
+SA Pacific Standard Time (UTC-05:00) Bogota, Lima, Quito, Rio Branco
+Eastern Standard Time (Mexico) (UTC-05:00) Chetumal
+Eastern Standard Time (UTC-05:00) Eastern Time (US & Canada)
+Haiti Standard Time (UTC-05:00) Haiti
+Cuba Standard Time (UTC-05:00) Havana
+US Eastern Standard Time (UTC-05:00) Indiana (East)
+Turks And Caicos Standard Time (UTC-05:00) Turks and Caicos
+Central America Standard Time (UTC-06:00) Central America
+Central Standard Time (UTC-06:00) Central Time (US & Canada)
+Easter Island Standard Time (UTC-06:00) Easter Island
+Central Standard Time (Mexico) (UTC-06:00) Guadalajara, Mexico City, Monterrey
+Canada Central Standard Time (UTC-06:00) Saskatchewan
+US Mountain Standard Time (UTC-07:00) Arizona
+Mountain Standard Time (Mexico) (UTC-07:00) La Paz, Mazatlan
+Mountain Standard Time (UTC-07:00) Mountain Time (US & Canada)
+Yukon Standard Time (UTC-07:00) Yukon
+Pacific Standard Time (Mexico) (UTC-08:00) Baja California
+UTC-08 (UTC-08:00) Co-ordinated Universal Time-08
+Pacific Standard Time (UTC-08:00) Pacific Time (US & Canada)
+Alaskan Standard Time (UTC-09:00) Alaska
+UTC-09 (UTC-09:00) Co-ordinated Universal Time-09
+Marquesas Standard Time (UTC-09:30) Marquesas Islands
+Aleutian Standard Time (UTC-10:00) Aleutian Islands
+Hawaiian Standard Time (UTC-10:00) Hawaii
+UTC-11 (UTC-11:00) Co-ordinated Universal Time-11
+Dateline Standard Time (UTC-12:00) International Date Line West
+UTC (UTC) Co-ordinated Universal Time
+GMT Standard Time (UTC+00:00) Dublin, Edinburgh, Lisbon, London
+Greenwich Standard Time (UTC+00:00) Monrovia, Reykjavik
+Sao Tome Standard Time (UTC+00:00) Sao Tome
+W. Europe Standard Time (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
+Central Europe Standard Time (UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
+Romance Standard Time (UTC+01:00) Brussels, Copenhagen, Madrid, Paris
+Morocco Standard Time (UTC+01:00) Casablanca
+Central European Standard Time (UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb
+W. Central Africa Standard Time (UTC+01:00) West Central Africa
+GTB Standard Time (UTC+02:00) Athens, Bucharest
+Middle East Standard Time (UTC+02:00) Beirut
+Egypt Standard Time (UTC+02:00) Cairo
+E. Europe Standard Time (UTC+02:00) Chisinau
+West Bank Standard Time (UTC+02:00) Gaza, Hebron
+South Africa Standard Time (UTC+02:00) Harare, Pretoria
+FLE Standard Time (UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius
+Israel Standard Time (UTC+02:00) Jerusalem
+South Sudan Standard Time (UTC+02:00) Juba
+Kaliningrad Standard Time (UTC+02:00) Kaliningrad
+Sudan Standard Time (UTC+02:00) Khartoum
+Libya Standard Time (UTC+02:00) Tripoli
+Namibia Standard Time (UTC+02:00) Windhoek
+Jordan Standard Time (UTC+03:00) Amman
+Arabic Standard Time (UTC+03:00) Baghdad
+Syria Standard Time (UTC+03:00) Damascus
+Turkey Standard Time (UTC+03:00) Istanbul
+Arab Standard Time (UTC+03:00) Kuwait, Riyadh
+Belarus Standard Time (UTC+03:00) Minsk
+Russian Standard Time (UTC+03:00) Moscow, St Petersburg
+E. Africa Standard Time (UTC+03:00) Nairobi
+Volgograd Standard Time (UTC+03:00) Volgograd
+Iran Standard Time (UTC+03:30) Tehran
+Arabian Standard Time (UTC+04:00) Abu Dhabi, Muscat
+Astrakhan Standard Time (UTC+04:00) Astrakhan, Ulyanovsk
+Azerbaijan Standard Time (UTC+04:00) Baku
+Russia Time Zone 3 (UTC+04:00) Izhevsk, Samara
+Mauritius Standard Time (UTC+04:00) Port Louis
+Saratov Standard Time (UTC+04:00) Saratov
+Georgian Standard Time (UTC+04:00) Tbilisi
+Caucasus Standard Time (UTC+04:00) Yerevan
+Afghanistan Standard Time (UTC+04:30) Kabul
+West Asia Standard Time (UTC+05:00) Ashgabat, Tashkent
+Qyzylorda Standard Time (UTC+05:00) Astana
+Ekaterinburg Standard Time (UTC+05:00) Ekaterinburg
+Pakistan Standard Time (UTC+05:00) Islamabad, Karachi
+India Standard Time (UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi
+Sri Lanka Standard Time (UTC+05:30) Sri Jayawardenepura
+Nepal Standard Time (UTC+05:45) Kathmandu
+Central Asia Standard Time (UTC+06:00) Bishkek
+Bangladesh Standard Time (UTC+06:00) Dhaka
+Omsk Standard Time (UTC+06:00) Omsk
+Myanmar Standard Time (UTC+06:30) Yangon (Rangoon)
+SE Asia Standard Time (UTC+07:00) Bangkok, Hanoi, Jakarta
+Altai Standard Time (UTC+07:00) Barnaul, Gorno-Altaysk
+W. Mongolia Standard Time (UTC+07:00) Hovd
+North Asia Standard Time (UTC+07:00) Krasnoyarsk
+N. Central Asia Standard Time (UTC+07:00) Novosibirsk
+Tomsk Standard Time (UTC+07:00) Tomsk
+China Standard Time (UTC+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi
+North Asia East Standard Time (UTC+08:00) Irkutsk
+Singapore Standard Time (UTC+08:00) Kuala Lumpur, Singapore
+W. Australia Standard Time (UTC+08:00) Perth
+Taipei Standard Time (UTC+08:00) Taipei
+Ulaanbaatar Standard Time (UTC+08:00) Ulaanbaatar
+Aus Central W. Standard Time (UTC+08:45) Eucla
+Transbaikal Standard Time (UTC+09:00) Chita
+Tokyo Standard Time (UTC+09:00) Osaka, Sapporo, Tokyo
+North Korea Standard Time (UTC+09:00) Pyongyang
+Korea Standard Time (UTC+09:00) Seoul
+Yakutsk Standard Time (UTC+09:00) Yakutsk
+Cen. Australia Standard Time (UTC+09:30) Adelaide
+AUS Central Standard Time (UTC+09:30) Darwin
+E. Australia Standard Time (UTC+10:00) Brisbane
+AUS Eastern Standard Time (UTC+10:00) Canberra, Melbourne, Sydney
+West Pacific Standard Time (UTC+10:00) Guam, Port Moresby
+Tasmania Standard Time (UTC+10:00) Hobart
+Vladivostok Standard Time (UTC+10:00) Vladivostok
+Lord Howe Standard Time (UTC+10:30) Lord Howe Island
+Bougainville Standard Time (UTC+11:00) Bougainville Island
+Russia Time Zone 10 (UTC+11:00) Chokurdakh
+Magadan Standard Time (UTC+11:00) Magadan
+Norfolk Standard Time (UTC+11:00) Norfolk Island
+Sakhalin Standard Time (UTC+11:00) Sakhalin
+Central Pacific Standard Time (UTC+11:00) Solomon Is., New Caledonia
+Russia Time Zone 11 (UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky
+New Zealand Standard Time (UTC+12:00) Auckland, Wellington
+UTC+12 (UTC+12:00) Co-ordinated Universal Time+12
+Fiji Standard Time (UTC+12:00) Fiji
+Kamchatka Standard Time (UTC+12:00) Petropavlovsk-Kamchatsky - Old
+Chatham Islands Standard Time (UTC+12:45) Chatham Islands
+UTC+13 (UTC+13:00) Co-ordinated Universal Time+13
+Tonga Standard Time (UTC+13:00) Nuku'alofa
+Samoa Standard Time (UTC+13:00) Samoa
+Line Islands Standard Time (UTC+14:00) Kiritimati Island`;
+
+ return timezones.split('\n').map(line => {
+ const parts = line.trim().split(/\s{2,}/);
+ if (parts.length >= 2) {
+ return {
+ value: parts[0].trim(),
+ label: parts[1].trim(),
+ };
+ }
+ return null;
+ }).filter(Boolean);
+};
+
+// Work days options
+const workDaysOptions = [
+ { value: "Sunday", label: "Sunday" },
+ { value: "Monday", label: "Monday" },
+ { value: "Tuesday", label: "Tuesday" },
+ { value: "Wednesday", label: "Wednesday" },
+ { value: "Thursday", label: "Thursday" },
+ { value: "Friday", label: "Friday" },
+ { value: "Saturday", label: "Saturday" },
+ { value: "WeekDay", label: "Weekdays (Monday-Friday)" },
+ { value: "WeekendDay", label: "Weekend (Saturday-Sunday)" },
+ { value: "AllDays", label: "All Days" }
+];
+
+// Automation Processing Options
+const automateProcessingOptions = [
+ { value: "None", label: "None - No processing" },
+ { value: "AutoUpdate", label: "AutoUpdate - Accept/Decline but not delete" },
+ { value: "AutoAccept", label: "AutoAccept - Accept and delete" }
+];
+
const EditRoomMailbox = () => {
const router = useRouter();
const { roomId } = router.query;
@@ -55,6 +233,29 @@ const EditRoomMailbox = () => {
isWheelChairAccessible: room.isWheelChairAccessible,
phone: room.phone,
tags: room.tags?.map(tag => ({ label: tag, value: tag })) || [],
+
+ // Calendar Properties
+ AllowConflicts: room.AllowConflicts,
+ AllowRecurringMeetings: room.AllowRecurringMeetings,
+ BookingWindowInDays: room.BookingWindowInDays,
+ MaximumDurationInMinutes: room.MaximumDurationInMinutes,
+ ProcessExternalMeetingMessages: room.ProcessExternalMeetingMessages,
+ EnforceCapacity: room.EnforceCapacity,
+ ForwardRequestsToDelegates: room.ForwardRequestsToDelegates,
+ ScheduleOnlyDuringWorkHours: room.ScheduleOnlyDuringWorkHours,
+ AutomateProcessing: room.AutomateProcessing,
+
+ // Calendar Configuration
+ WorkDays: room.WorkDays?.split(',')?.map(day => ({
+ label: day.trim(),
+ value: day.trim()
+ })) || [],
+ WorkHoursStartTime: room.WorkHoursStartTime,
+ WorkHoursEndTime: room.WorkHoursEndTime,
+ WorkingHoursTimeZone: room.WorkingHoursTimeZone ? {
+ value: room.WorkingHoursTimeZone,
+ label: createTimezoneOptions().find(tz => tz.value === room.WorkingHoursTimeZone)?.label || room.WorkingHoursTimeZone
+ } : null
});
}
}, [roomInfo.isSuccess, roomInfo.data]);
@@ -101,11 +302,32 @@ const EditRoomMailbox = () => {
isWheelChairAccessible: values.isWheelChairAccessible,
phone: values.phone?.trim(),
tags: values.tags?.map(tag => tag.value),
+
+ // Calendar Properties
+ AllowConflicts: values.AllowConflicts,
+ AllowRecurringMeetings: values.AllowRecurringMeetings,
+ BookingWindowInDays: values.BookingWindowInDays,
+ MaximumDurationInMinutes: values.MaximumDurationInMinutes,
+ ProcessExternalMeetingMessages: values.ProcessExternalMeetingMessages,
+ EnforceCapacity: values.EnforceCapacity,
+ ForwardRequestsToDelegates: values.ForwardRequestsToDelegates,
+ ScheduleOnlyDuringWorkHours: values.ScheduleOnlyDuringWorkHours,
+ AutomateProcessing: values.AutomateProcessing?.value || values.AutomateProcessing,
+
+ // Calendar Configuration
+ WorkDays: values.WorkDays?.map(day => day.value).join(','),
+ WorkHoursStartTime: values.WorkHoursStartTime,
+ WorkHoursEndTime: values.WorkHoursEndTime,
+ WorkingHoursTimeZone: values.WorkingHoursTimeZone?.value || values.WorkingHoursTimeZone,
})}
>
- {/* Core & Booking Settings */}
-
+ {/* Basic Information */}
+
+ Basic Information
+
+
+
{
/>
-
+
+
+
+
+
+
+ {/* Booking Settings */}
+
+ Booking Settings
+
+
+
{
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- {/* Location Information */}
-
- Location Information
+ {/* Working Hours */}
+
+ Working Hours
- {/* Building and Floor Info */}
-
+
-
-
-
-
-
-
-
-
-
-
-
- {/* Address Fields */}
-
+
-
- {/* City and Postal Code */}
-
-
-
-
-
-
-
-
- {/* State and Country */}
-
-
-
-
-
- ({
- label: Name,
- value: Code,
- }))}
- formControl={formControl}
- />
-
-
+
+
+
+
+
+
+
+
+
+
- {/* Room Equipment */}
-
- Room Equipment
+ {/* Room Facilities */}
+
+ Room Facilities & Equipment
+
+
+
+
+
+
+
+
-
+
{
/>
-
+
{
/>
-
+
{
/>
+
+
+
+
- {/* Room Features */}
-
- Room Features
+ {/* Location Information */}
+
+ Location Information
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
({
+ label: Name,
+ value: Code,
+ }))}
formControl={formControl}
- multiple={true}
- creatable={true}
/>
@@ -308,4 +668,4 @@ const EditRoomMailbox = () => {
EditRoomMailbox.getLayout = (page) => {page};
-export default EditRoomMailbox;
\ No newline at end of file
+export default EditRoomMailbox;
\ No newline at end of file
diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js
index bbf9e9f2d1c2..68c79ba360c2 100644
--- a/src/pages/email/resources/management/list-rooms/index.js
+++ b/src/pages/email/resources/management/list-rooms/index.js
@@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import { Button } from "@mui/material";
import Link from "next/link";
-import { AddHomeWork, Edit, Block, LockOpen } from "@mui/icons-material";
+import { AddHomeWork, Edit, Block, LockOpen, Key } from "@mui/icons-material";
import { TrashIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -16,6 +16,12 @@ const Page = () => {
color: "info",
condition: (row) => !row.isDirSynced,
},
+ {
+ label: "Edit permissions",
+ link: "/identity/administration/users/user/exchange?userId=[id]",
+ color: "info",
+ icon: ,
+ },
{
label: "Block Sign In",
type: "POST",
diff --git a/src/pages/email/spamfilter/list-connectionfilter/add.jsx b/src/pages/email/spamfilter/list-connectionfilter/add.jsx
index c2e2c9a5785f..8937226563a9 100644
--- a/src/pages/email/spamfilter/list-connectionfilter/add.jsx
+++ b/src/pages/email/spamfilter/list-connectionfilter/add.jsx
@@ -1,5 +1,6 @@
import React, { useEffect } from "react";
-import { Grid, Divider } from "@mui/material";
+import { Divider } from "@mui/material";
+import { Grid } from "@mui/system";
import { useForm, useWatch } from "react-hook-form";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
@@ -33,7 +34,7 @@ const AddPolicy = () => {
postUrl="/api/AddConnectionFilter"
>
-
+
{
{/* TemplateList */}
-
+
{
-
+
{
+ const formControl = useForm({
+ mode: "onChange",
+ defaultValues: {
+ selectedTenants: [],
+ TemplateList: null,
+ PowerShellCommand: "",
+ },
+ });
+
+ const templateListVal = useWatch({ control: formControl.control, name: "TemplateList" });
+
+ useEffect(() => {
+ if (templateListVal?.value) {
+ formControl.setValue("PowerShellCommand", JSON.stringify(templateListVal?.value));
+ }
+ }, [templateListVal, formControl]);
+
+ // Watch the value of QuarantineNotification
+ const quarantineNotification = useWatch({
+ control: formControl.control,
+ name: "QuarantineNotification",
+ });
+
+ return (
+
+
+
+
+
+
+ {/* */}
+
+ {/* TemplateList, can be added later. But did not seem necessary with so few settings */}
+ {/*
+ option,
+ url: "/api/ListSpamFilterTemplates",
+ }}
+ placeholder="Select a template or enter PowerShell JSON manually"
+ />
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+AddPolicy.getLayout = (page) => {page};
+
+export default AddPolicy;
diff --git a/src/pages/email/spamfilter/list-quarantine-policies/index.js b/src/pages/email/spamfilter/list-quarantine-policies/index.js
new file mode 100644
index 000000000000..29be070c7f1f
--- /dev/null
+++ b/src/pages/email/spamfilter/list-quarantine-policies/index.js
@@ -0,0 +1,435 @@
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import CippButtonCard from "/src/components/CippCards/CippButtonCard";
+import { CippInfoBar } from "/src/components/CippCards/CippInfoBar";
+import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx";
+import { Alert, Typography, Stack, Tooltip, IconButton, SvgIcon, Button } from "@mui/material";
+import { Grid } from "@mui/system";
+import Link from "next/link";
+import {
+ AccessTime,
+ CorporateFare,
+ AlternateEmail,
+ Language,
+ Sync,
+ RocketLaunch,
+ Edit,
+ Delete,
+} from "@mui/icons-material";
+import { useSettings } from "/src/hooks/use-settings";
+import { useDialog } from "/src/hooks/use-dialog";
+import { ApiGetCall } from "/src/api/ApiCall";
+
+const Page = () => {
+ const pageTitle = "Quarantine Policies";
+ const { currentTenant } = useSettings();
+
+ const createDialog = useDialog();
+
+ // Use ApiGetCall directly as a hook
+ const GlobalQuarantinePolicy = ApiGetCall({
+ url: "/api/ListQuarantinePolicy",
+ data: { tenantFilter: currentTenant, type: "GlobalQuarantinePolicy" },
+ queryKey: "GlobalQuarantinePolicy",
+ });
+
+ // Get the policy data regardless of array or object
+ const globalQuarantineData = Array.isArray(GlobalQuarantinePolicy.data)
+ ? GlobalQuarantinePolicy.data[0]
+ : GlobalQuarantinePolicy.data;
+
+ const hasGlobalQuarantinePolicyData = !!globalQuarantineData;
+
+
+ if (hasGlobalQuarantinePolicyData) {
+ globalQuarantineData.EndUserSpamNotificationFrequency =
+ globalQuarantineData?.EndUserSpamNotificationFrequency === "P1D"
+ ? "Daily"
+ : globalQuarantineData?.EndUserSpamNotificationFrequency === "P7D"
+ ? "Weekly"
+ : globalQuarantineData?.EndUserSpamNotificationFrequency === "PT4H"
+ ? "4 hours"
+ : globalQuarantineData?.EndUserSpamNotificationFrequency
+ }
+
+ const multiLanguagePropertyItems = hasGlobalQuarantinePolicyData
+ ? (
+ Array.isArray(globalQuarantineData?.MultiLanguageSetting) && globalQuarantineData.MultiLanguageSetting.length > 0
+ ? globalQuarantineData.MultiLanguageSetting.map((language, idx) => ({
+ language: language == "Default" ? "English_USA"
+ : language == "English" ? "English_GB"
+ : language,
+ senderDisplayName:
+ globalQuarantineData.MultiLanguageSenderName[idx] &&
+ globalQuarantineData.MultiLanguageSenderName[idx].trim() !== ""
+ ? globalQuarantineData.MultiLanguageSenderName[idx]
+ : "None",
+ subject:
+ globalQuarantineData.EsnCustomSubject[idx] &&
+ globalQuarantineData.EsnCustomSubject[idx].trim() !== ""
+ ? globalQuarantineData.EsnCustomSubject[idx]
+ : "None",
+ disclaimer:
+ globalQuarantineData.MultiLanguageCustomDisclaimer[idx] &&
+ globalQuarantineData.MultiLanguageCustomDisclaimer[idx].trim() !== ""
+ ? globalQuarantineData.MultiLanguageCustomDisclaimer[idx]
+ : "None",
+ }))
+ : [
+ {
+ language: "None",
+ senderDisplayName: "None",
+ subject: "None",
+ disclaimer: "None",
+ },
+ ]
+ )
+ : [];
+
+ const buttonCardActions = [
+ <>
+ }>
+ Edit Settings
+
+
+ {
+ GlobalQuarantinePolicy.refetch();
+ }}
+ >
+
+
+
+
+
+ >
+ ];
+
+ // Actions to perform (Edit,Delete Policy)
+ const actions = [
+ {
+ label: "Edit Policy",
+ type: "POST",
+ url: "/api/EditQuarantinePolicy?type=QuarantinePolicy",
+ setDefaultValues: true,
+ fields: [
+ {
+ type: "textField",
+ name: "Name",
+ label: "Policy Name",
+ disabled: true,
+ },
+ {
+ type: "autoComplete",
+ name: "ReleaseActionPreference",
+ label: "Select release action preference",
+ multiple : false,
+ creatable : false,
+ options: [
+ { label: "Release", value: "Release" },
+ { label: "Request Release", value: "RequestRelease" },
+ ],
+ },
+ {
+ type: "switch",
+ name: "Delete",
+ label: "Delete",
+ },
+ {
+ type: "switch",
+ name: "Preview",
+ label: "Preview",
+ },
+ {
+ type: "switch",
+ name: "BlockSender",
+ label: "Block Sender",
+ },
+ {
+ type: "switch",
+ name: "AllowSender",
+ label: "Allow Sender",
+ },
+ {
+ type: "switch",
+ name: "QuarantineNotification",
+ label: "Quarantine Notification",
+ },
+ {
+ type: "switch",
+ name: "IncludeMessagesFromBlockedSenderAddress",
+ label: "Include Messages From Blocked Sender Address",
+ },
+ ],
+ data: { Identity: "Guid", Action: "!Edit" },
+ confirmText: "Update Quarantine Policy '[Name]'? Policy Name cannot be changed.",
+ multiPost: false,
+ icon: ,
+ color: "info",
+ condition: (row) => row.Guid != "00000000-0000-0000-0000-000000000000",
+ },
+ {
+ label: "Delete Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/RemoveQuarantinePolicy",
+ data: {
+ Name: "Name",
+ Identity: "Guid",
+ },
+ confirmText: (
+ <>
+
+ Are you sure you want to delete this policy?
+
+
+ Note: This will delete the Quarantine policy, even if it is currently in use.
+ Removing the Admin and User Access it applies to emails.
+
+
+ Confirm the Quarantine is not applied in any of the following policies:
+
+ - Anti-phishing
+ - Anti-spam
+ - Anti-malware
+ - Safe Attachments
+
+
+ >
+ ),
+ condition: (row) => row.Guid != "00000000-0000-0000-0000-000000000000",
+ },
+ ];
+
+ // Off-canvas structure: displays extended details and includes actions (Enable/Disable Rule)
+ const offCanvas = {
+ extendedInfoFields: [
+ "Id", // Policy Name/Id
+ "Name", // Policy Name
+ "EndUserQuarantinePermissions",
+ "Guid",
+ "Builtin",
+ "WhenCreated", // Creation Date
+ "WhenChanged", // Last Modified Date
+ ],
+ actions: actions,
+ };
+
+ const filterList = [
+ {
+ filterName: "Custom Policies",
+ value: [{ id: "Builtin", value: "No" }],
+ type: "column",
+ },
+ {
+ filterName: "Built-in Policies",
+ value: [{ id: "Builtin", value: "Yes" }],
+ type: "column",
+ },
+ ];
+
+
+ const customLanguageOffcanvas =
+ multiLanguagePropertyItems && multiLanguagePropertyItems.length > 0
+ ? {
+ offcanvas: {
+ title: "Custom Language Settings",
+ propertyItems: multiLanguagePropertyItems.map((item, idx) => ({
+ label: "",
+ value: (
+
+
+ {item.language}
+
+ }
+ cardSx={{ mb: 2 }}
+ >
+