diff --git a/cspell.json b/cspell.json index 921542d92e48..1ff07bf80063 100644 --- a/cspell.json +++ b/cspell.json @@ -4,28 +4,61 @@ "dictionaryDefinitions": [], "dictionaries": [], "words": [ - "CIPP", - "CIPP-API", - "Entra", - "Intune", - "GDAP", - "OBEE", - "AITM", - "Passwordless", - "Yubikey", - "Sherweb", - "Autotask", - "Datto", - "Syncro", - "ImmyBot", - "Choco", + "ADMS", + "AITM", + "Augmentt", + "Autotask", + "Choco", + "CIPP", + "CIPP-API", + "Datto", + "Entra", + "ESET", + "GDAP", + "HIBP", + "Hudu", + "ImmyBot", + "Intune", + "LCID", + "OBEE", + "Passwordless", + "pwpush", + "Rewst", + "Sherweb", + "Syncro", + "Yubikey" ], "ignoreWords": [ - "CIPPAPI", - "locationcipp", - "TNEF", - "winmail", - "PSTN", + "Addins", + "CIPPAPI", + "PSTN", + "TNEF", + "exo_individualsharing", + "exo_mailboxaudit", + "exo_mailtipsenabled", + "exo_outlookaddins", + "exo_storageproviderrestricted", + "locationcipp", + "mdo_antiphishingpolicies", + "mdo_autoforwardingmode", + "mdo_blockmailforward", + "mdo_commonattachmentsfilter", + "mdo_highconfidencephishaction", + "mdo_highconfidencespamaction", + "mdo_phishthresholdlevel", + "mdo_phisspamacation", + "mdo_safeattachmentpolicy", + "mdo_safeattachments", + "mdo_safedocuments", + "mdo_safelinksforOfficeApps", + "mdo_safelinksforemail", + "mdo_spam_notifications_only_for_admins", + "mdo_zapmalware", + "mdo_zapphish", + "mdo_zapspam", + "microsoftonline", + "mip_search_auditlog", + "winmail" ], "import": [] } diff --git a/package.json b/package.json index 879b3c54028c..934681339eba 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,10 @@ "@musement/iso-duration": "^1.0.0", "@react-pdf/renderer": "4.3.0", "@reduxjs/toolkit": "2.6.1", + "@tanstack/query-sync-storage-persister": "^5.76.0", "@tanstack/react-query": "^5.51.11", "@tanstack/react-query-devtools": "^5.51.11", + "@tanstack/react-query-persist-client": "^5.76.0", "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^2.9.1", "@tiptap/extension-heading": "^2.9.1", diff --git a/public/assets/illustrations/undraw-into-the-night-nd84.svg b/public/assets/illustrations/undraw-into-the-night-nd84.svg new file mode 100644 index 000000000000..a7d05162299b --- /dev/null +++ b/public/assets/illustrations/undraw-into-the-night-nd84.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/version.json b/public/version.json index fea50d299123..fb57e409303d 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "7.5.3" -} \ No newline at end of file + "version": "8.0.0" +} diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 936654ff16fd..35554764168e 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -1,4 +1,10 @@ -import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import axios, { isAxiosError } from "axios"; import { useDispatch } from "react-redux"; import { showToast } from "../store/toasts"; @@ -15,7 +21,7 @@ export function ApiGetCall(props) { bulkRequest = false, toast = false, onResult, - staleTime = 600000, // 10 minutes + staleTime = 300000, refetchOnWindowFocus = false, refetchOnMount = true, refetchOnReconnect = true, @@ -212,7 +218,7 @@ export function ApiGetCallWithPagination({ } return lastPage?.Metadata?.nextLink ? { nextLink: lastPage.Metadata.nextLink } : undefined; }, - staleTime: 600000, // 10 minutes + staleTime: 300000, refetchOnWindowFocus: false, retry: retryFn, }); diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx index 03fd08306afc..93bcc3cd7104 100644 --- a/src/components/CippCards/CippDomainCards.jsx +++ b/src/components/CippCards/CippDomainCards.jsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import { - Grid, Button, Collapse, Switch, @@ -13,6 +12,7 @@ import { Divider, FormControlLabel, } from "@mui/material"; +import { Grid } from "@mui/system"; import SearchIcon from "@mui/icons-material/Search"; import ClearIcon from "@mui/icons-material/Clear"; import SettingsIcon from "@mui/icons-material/Settings"; @@ -152,7 +152,7 @@ function DomainResultCard({ title, data, isFetching, info, type }) { ? { children: ( - + {info} @@ -182,12 +182,11 @@ function DomainResultCard({ title, data, isFetching, info, type }) { ? { children: ( //4 headers, "Record" and then under it. - <> + (<> Record: - - + ) ), } : type === "SPF" @@ -362,7 +361,7 @@ function DomainResultCard({ title, data, isFetching, info, type }) { } isFetching={isFetching} > - + {info} setVisible(false)} {...offCanvasData} /> @@ -484,7 +483,7 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) return (
- + - + - + @@ -563,7 +562,7 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) {domain && ( <> - + - + - + - + - + - + - + - + {enableHttps && ( - + { { {data.map((item, index) => ( <> setVisibleIndex(index) : undefined} sx={{ @@ -45,20 +43,39 @@ export const CippInfoBar = ({ data, isFetching }) => { {item.icon} )} - { - if (!item?.icon) { - return { pl: 2 }; - } - }} - > - - {item.name} - - - {isFetching ? : item.data} - - + {item?.toolTip ? ( + + { + if (!item?.icon) { + return { pl: 2 }; + } + }} + > + + {item.name} + + + {isFetching ? : item.data} + + + + ) : ( + { + if (!item?.icon) { + return { pl: 2 }; + } + }} + > + + {item.name} + + + {isFetching ? : item.data} + + + )} {item.offcanvas && ( @@ -78,7 +95,7 @@ export const CippInfoBar = ({ data, isFetching }) => { }} > - + {item?.offcanvas?.propertyItems?.length > 0 && ( { ) : ( // Two-column layout - { )) )} - + ) )} diff --git a/src/components/CippCards/CippUniversalSearch.jsx b/src/components/CippCards/CippUniversalSearch.jsx index c4b5ddbaf294..ccba59600fc0 100644 --- a/src/components/CippCards/CippUniversalSearch.jsx +++ b/src/components/CippCards/CippUniversalSearch.jsx @@ -3,7 +3,6 @@ import { TextField, Box, Typography, - Grid, Card, CardContent, CardHeader, @@ -11,6 +10,7 @@ import { Button, Link, } from "@mui/material"; +import { Grid } from "@mui/system"; import { ApiGetCall } from "../../api/ApiCall"; export const CippUniversalSearch = React.forwardRef( @@ -97,7 +97,7 @@ const Results = ({ items = [], searchValue }) => { {displayedResults.map((item, key) => ( - + ))} diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx index f0de3777f71a..97e6917e9126 100644 --- a/src/components/CippCards/CippUserInfoCard.jsx +++ b/src/components/CippCards/CippUserInfoCard.jsx @@ -130,6 +130,14 @@ export const CippUserInfoCard = (props) => { label="Postal code" value={isFetching ? : user?.postalCode || "N/A"} /> + : user?.city || "N/A"} + /> + : user?.country || "N/A"} + /> { + const [selectedPermissionSet, setSelectedPermissionSet] = useState(null); + const [permissionsLoaded, setPermissionsLoaded] = useState(false); + + // When templateData changes, update the form + useEffect(() => { + if (!isEditing && !isCopy) { + formControl.setValue("templateName", "New App Deployment Template"); + formControl.setValue("appType", "EnterpriseApp"); + setPermissionsLoaded(false); + } else if (templateData && isCopy) { + // When copying, we want to load the template data but not the ID + if (templateData[0]) { + const copyName = `Copy of ${templateData[0].TemplateName}`; + formControl.setValue("templateName", copyName); + formControl.setValue("appId", { + label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, + value: templateData[0].AppId, + addedFields: { + displayName: templateData[0].AppName, + }, + }); + + // Set permission set and trigger loading of permissions + const permissionSetValue = { + label: templateData[0].PermissionSetName || "Custom Permissions", + value: templateData[0].PermissionSetId, + addedFields: { + Permissions: templateData[0].Permissions || {}, + }, + }; + + formControl.setValue("permissionSetId", permissionSetValue); + setSelectedPermissionSet(permissionSetValue); + setPermissionsLoaded(true); + } + } else if (templateData) { + // For editing, load all template data + if (templateData[0]) { + formControl.setValue("templateName", templateData[0].TemplateName); + formControl.setValue("appId", { + label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, + value: templateData[0].AppId, + addedFields: { + displayName: templateData[0].AppName, + }, + }); + + // Set permission set and trigger loading of permissions + const permissionSetValue = { + label: templateData[0].PermissionSetName || "Custom Permissions", + value: templateData[0].PermissionSetId, + addedFields: { + Permissions: templateData[0].Permissions || {}, + }, + }; + + formControl.setValue("permissionSetId", permissionSetValue); + setSelectedPermissionSet(permissionSetValue); + setPermissionsLoaded(true); + } + } + }, [templateData, isCopy, isEditing, formControl]); + + // Watch for app selection changes to update template name + const selectedApp = formControl.watch("appId"); + + useEffect(() => { + // Update template name when app is selected if we're in add mode and name hasn't been manually changed + if (selectedApp && !isEditing && !isCopy) { + const currentName = formControl.getValues("templateName"); + // Only update if it's still the default or empty + if (currentName === "New App Deployment Template" || !currentName) { + // Extract app name from the label (format is usually "AppName (AppId)") + const appName = selectedApp.label.split(" (")[0]; + if (appName) { + formControl.setValue("templateName", `${appName} Template`); + } + } + } + }, [selectedApp, isEditing, isCopy, formControl]); + + // Watch for permission set selection changes + const selectedPermissionSetValue = formControl.watch("permissionSetId"); + + useEffect(() => { + if (selectedPermissionSetValue?.value) { + setSelectedPermissionSet(selectedPermissionSetValue); + setPermissionsLoaded(true); + } else { + setSelectedPermissionSet(null); + setPermissionsLoaded(false); + } + }, [selectedPermissionSetValue]); + + // Handle initial data loading for editing and copying + useEffect(() => { + // When editing or copying, ensure permission data is properly loaded + if (isEditing || isCopy) { + if (templateData?.[0]?.Permissions) { + // Ensure permissions are immediately available for the preview + setPermissionsLoaded(true); + } + } + }, [isEditing, isCopy, templateData]); + + // Handle form submission + const handleSubmit = (data) => { + const appDisplayName = + data.appId?.addedFields?.displayName || + (data.appId?.label ? data.appId.label.split(" (")[0] : undefined); + + const payload = { + TemplateName: data.templateName, + AppId: data.appId?.value, + AppName: appDisplayName, + PermissionSetId: data.permissionSetId?.value, + PermissionSetName: data.permissionSetId?.label, + Permissions: data.permissionSetId?.addedFields?.Permissions, + }; + + if (isEditing && !isCopy && templateData?.[0]?.TemplateId) { + payload.TemplateId = templateData[0].TemplateId; + } + + // Store values before submission to set them back afterward + const currentValues = { + templateName: data.templateName, + appId: data.appId, + permissionSetId: data.permissionSetId, + }; + + onSubmit(payload); + + // After submission, set the values back to what they were but mark as clean + // This will only apply to add page, as edit will get refreshed data + if (!isEditing) { + setTimeout(() => { + formControl.setValue("templateName", currentValues.templateName, { shouldDirty: false }); + formControl.setValue("appId", currentValues.appId, { shouldDirty: false }); + formControl.setValue("permissionSetId", currentValues.permissionSetId, { + shouldDirty: false, + }); + }, 100); + } + }; + + return ( + + + + App Approval Template Details + {templateLoading && } + {(!templateLoading || !isEditing) && ( + <> + + App approval templates allow you to define an application with its permissions that + can be deployed to multiple tenants. Select an application and permission set to + create a template. + + + + + `${item.displayName} (${item.appId})`, + valueField: "appId", + addedField: { + displayName: "displayName", + }, + showRefresh: true, + }} + multiple={false} + validators={{ required: "Application is required" }} + /> + + item.TemplateName, + valueField: "TemplateId", + addedField: { + Permissions: "Permissions", + }, + showRefresh: true, + }} + multiple={false} + validators={{ required: "Permission Set is required" }} + /> + + + + + + + + + )} + + + + + + + ); +}; + +export default AppApprovalTemplateForm; diff --git a/src/components/CippComponents/BPASyncDialog.jsx b/src/components/CippComponents/BPASyncDialog.jsx index 10ae3cecd724..014e033747e6 100644 --- a/src/components/CippComponents/BPASyncDialog.jsx +++ b/src/components/CippComponents/BPASyncDialog.jsx @@ -29,7 +29,7 @@ export const BPASyncDialog = ({ createDialog }) => { const [isSyncing, setIsSyncing] = useState(false); const bpaSyncResults = ApiPostCall({ - urlfromdata: true, + urlFromData: true, }); const handleForm = (values) => { diff --git a/src/components/CippComponents/CIPPDeviceCodeButton.js b/src/components/CippComponents/CIPPDeviceCodeButton.js new file mode 100644 index 000000000000..e262b69c7912 --- /dev/null +++ b/src/components/CippComponents/CIPPDeviceCodeButton.js @@ -0,0 +1,253 @@ +import { useState, useEffect } from "react"; +import { + Alert, + Button, + Stack, + Typography, + CircularProgress, + Box, +} from "@mui/material"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; + +/** + * CIPPDeviceCodeButton - A button component for Microsoft 365 OAuth authentication using device code flow + * + * @param {Object} props - Component props + * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data + * @param {Function} props.onAuthError - Callback function called when authentication fails with error data + * @param {string} props.buttonText - Text to display on the button (default: "Login with Device Code") + * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true) + * @returns {JSX.Element} The CIPPDeviceCodeButton component + */ +export const CIPPDeviceCodeButton = ({ + onAuthSuccess, + onAuthError, + buttonText = "Login with Device Code", + showResults = true, +}) => { + const [authInProgress, setAuthInProgress] = useState(false); + const [authError, setAuthError] = useState(null); + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [pollInterval, setPollInterval] = useState(null); + const [tokens, setTokens] = useState({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Get application ID information from API + const appIdInfo = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle closing the error + const handleCloseError = () => { + setAuthError(null); + }; + + // Clear polling interval when component unmounts + useEffect(() => { + return () => { + if (pollInterval) { + clearInterval(pollInterval); + } + }; + }, [pollInterval]); + + // Start device code authentication + const startDeviceCodeAuth = async () => { + try { + setAuthInProgress(true); + setAuthError(null); + setDeviceCodeInfo(null); + setCurrentStep(1); + + // Call the API to start device code flow + const response = await fetch(`/api/ExecSAMSetup?CreateSAM=true`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (response.ok && data.code) { + // Store device code info + setDeviceCodeInfo({ + user_code: data.code, + verification_uri: data.url, + expires_in: 900, // Default to 15 minutes if not provided + }); + + // Start polling for token + const interval = setInterval(checkAuthStatus, 5000); + setPollInterval(interval); + } else { + // Error getting device code + setAuthError({ + errorCode: "device_code_error", + errorMessage: data.message || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError(error); + } + } catch (error) { + console.error("Error starting device code authentication:", error); + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred during device code authentication", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError(error); + } + }; + + // Check authentication status + const checkAuthStatus = async () => { + try { + // Call the API to check auth status + const response = await fetch(`/api/ExecSAMSetup?CheckSetupProcess=true&step=1`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (response.ok) { + if (data.step === 2) { + // Authentication successful + clearInterval(pollInterval); + setPollInterval(null); + + // Process token data + const tokenData = { + accessToken: "Successfully authenticated", + refreshToken: "Token stored on server", + accessTokenExpiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour from now + refreshTokenExpiresOn: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now + username: "authenticated user", + tenantId: data.tenantId || "unknown", + onmicrosoftDomain: null, + }; + + // Store tokens in component state + setTokens(tokenData); + setDeviceCodeInfo(null); + setCurrentStep(2); + + // Call the onAuthSuccess callback if provided + if (onAuthSuccess) onAuthSuccess(tokenData); + + // Update UI state + setAuthInProgress(false); + } + } else { + // Error checking auth status + clearInterval(pollInterval); + setPollInterval(null); + + setAuthError({ + errorCode: "auth_status_error", + errorMessage: data.message || "Failed to check authentication status", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError({ + errorCode: "auth_status_error", + errorMessage: data.message || "Failed to check authentication status", + timestamp: new Date().toISOString(), + }); + } + } catch (error) { + console.error("Error checking auth status:", error); + // Don't stop polling on transient errors + } + }; + + return ( +
+ + + {!appIdInfo.isLoading && + !appIdInfo?.data?.applicationId && ( + + The Application ID is not valid. Please check your configuration. + + ) + } + + {showResults && ( + + {deviceCodeInfo && authInProgress ? ( + + Device Code Authentication + + To sign in, use a web browser to open the page {deviceCodeInfo.verification_uri} and enter the code {deviceCodeInfo.user_code} to authenticate. + + + Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes + + + ) : tokens.accessToken ? ( + + Authentication Successful + + You've successfully refreshed your token using device code flow. + + {tokens.tenantId && ( + + Tenant ID: {tokens.tenantId} + + )} + + ) : authError ? ( + + Authentication Error: {authError.errorCode} + {authError.errorMessage} + + Time: {authError.timestamp} + + + + + + ) : null} + + )} +
+ ); +}; + +export default CIPPDeviceCodeButton; \ No newline at end of file diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx new file mode 100644 index 000000000000..88e517a2139d --- /dev/null +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -0,0 +1,683 @@ +import { useState, useEffect } from "react"; +import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; +import { CippCopyToClipBoard } from "./CippCopyToClipboard"; + +export const CIPPM365OAuthButton = ({ + onAuthSuccess, + onAuthError, + buttonText = "Login with Microsoft", + showResults = true, + showSuccessAlert = true, + scope = "https://graph.microsoft.com/.default offline_access profile openid", + useDeviceCode = false, + applicationId = null, + autoStartDeviceLogon = false, + validateServiceAccount = true, +}) => { + const [authInProgress, setAuthInProgress] = useState(false); + const [authError, setAuthError] = useState(null); + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); + const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false); + const [isServiceAccount, setIsServiceAccount] = useState(true); + const [tokens, setTokens] = useState({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + const appIdInfo = ApiGetCall({ + url: `/api/ExecListAppId`, + waiting: true, + }); + + useEffect(() => { + appIdInfo.refetch(); + }, []); + + const handleCloseError = () => { + setAuthError(null); + }; + + const checkIsServiceAccount = (username) => { + if (!username || !validateServiceAccount) return true; // If no username or validation disabled, don't show warning + + const lowerUsername = username.toLowerCase(); + return lowerUsername.includes("service") || lowerUsername.includes("cipp"); + }; + + // Function to retrieve device code + const retrieveDeviceCode = async () => { + setCodeRetrievalInProgress(true); + setAuthError(null); + + // Refetch appId to ensure we have the latest + await appIdInfo.refetch(); + + try { + // Get the application ID to use + const appId = + applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + + // Request device code from our API endpoint + const deviceCodeResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent( + scope + )}` + ); + const deviceCodeData = await deviceCodeResponse.json(); + + if (deviceCodeResponse.ok && deviceCodeData.user_code) { + // Store device code info + setDeviceCodeInfo(deviceCodeData); + } else { + // Error getting device code + setAuthError({ + errorCode: deviceCodeData.error || "device_code_error", + errorMessage: deviceCodeData.error_description || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + } + } catch (error) { + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred retrieving device code", + timestamp: new Date().toISOString(), + }); + } finally { + setCodeRetrievalInProgress(false); + } + }; + + // Device code authentication function - opens popup and starts polling + const handleDeviceCodeAuthentication = async () => { + // Refetch appId to ensure we have the latest + await appIdInfo.refetch(); + + if (!deviceCodeInfo) { + // If we don't have a device code yet, retrieve it first + await retrieveDeviceCode(); + return; + } + + setAuthInProgress(true); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + try { + // Get the application ID to use - refetch already happened at the start of this function + const appId = + applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + + // Open popup to device login page + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + "https://microsoft.com/devicelogin", + "deviceLoginPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Start polling for token + const pollInterval = deviceCodeInfo.interval || 5; + const expiresIn = deviceCodeInfo.expires_in || 900; + const startTime = Date.now(); + + const pollForToken = async () => { + // Check if we've exceeded the expiration time + if (Date.now() - startTime >= expiresIn * 1000) { + if (popup && !popup.closed) { + popup.close(); + } + setAuthError({ + errorCode: "timeout", + errorMessage: "Device code authentication timed out", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + return; + } + + try { + // Poll for token using our API endpoint + const tokenResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}` + ); + const tokenData = await tokenResponse.json(); + + if (tokenResponse.ok && tokenData.status === "success") { + // Successfully got token + if (popup && !popup.closed) { + popup.close(); + } + handleTokenResponse(tokenData); + } else if ( + tokenData.error === "authorization_pending" || + tokenData.status === "pending" + ) { + // User hasn't completed authentication yet, continue polling + setTimeout(pollForToken, pollInterval * 1000); + } else if (tokenData.error === "slow_down") { + // Server asking us to slow down polling + setTimeout(pollForToken, (pollInterval + 5) * 1000); + } else { + // Other error + if (popup && !popup.closed) { + popup.close(); + } + setAuthError({ + errorCode: tokenData.error || "token_error", + errorMessage: tokenData.error_description || "Failed to get token", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + } catch (error) { + setTimeout(pollForToken, pollInterval * 1000); + } + }; + + // Start polling + setTimeout(pollForToken, pollInterval * 1000); + } catch (error) { + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred during device code authentication", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + }; + + // Process token response (common for both auth methods) + const handleTokenResponse = (tokenData) => { + // Extract token information + const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000); + // Refresh tokens typically last for 90 days, but this can vary + const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); + + // Extract information from ID token if available + let username = "unknown user"; + let tenantId = "unknown tenant"; + let onmicrosoftDomain = null; + + if (tokenData.id_token) { + try { + const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); + + username = + idTokenPayload.preferred_username || + idTokenPayload.email || + idTokenPayload.upn || + idTokenPayload.name || + "unknown user"; + + if (idTokenPayload.tid) { + tenantId = idTokenPayload.tid; + } + + if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { + onmicrosoftDomain = username.split("@")[1]; + } else if (idTokenPayload.iss) { + const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//); + if (issuerMatch && issuerMatch[1]) { + } + } + setIsServiceAccount(checkIsServiceAccount(username)); + } catch (error) {} + } + + // Create token result object + const tokenResult = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshTokenExpiresOn: refreshTokenExpiresOn, + username: username, + tenantId: tenantId, + onmicrosoftDomain: onmicrosoftDomain, + }; + + setTokens(tokenResult); + setDeviceCodeInfo(null); + + if (onAuthSuccess) onAuthSuccess(tokenResult); + + // Update UI state + setAuthInProgress(false); + setIsServiceAccount(checkIsServiceAccount(username)); + }; + + // MSAL-like authentication function + const handleMsalAuthentication = async () => { + // Clear previous authentication state when starting a new authentication + setAuthInProgress(true); + setAuthError(null); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Refetch app ID info to ensure we have the latest + await appIdInfo.refetch(); + + // Get the application ID to use - now we're sure to have the latest after the await + const appId = applicationId || appIdInfo?.data?.applicationId; + + // Generate MSAL-like authentication parameters + const msalConfig = { + auth: { + clientId: appId, + authority: `https://login.microsoftonline.com/common`, + redirectUri: `${window.location.origin}/authredirect`, + }, + }; + + // Define the request object similar to MSAL + const loginRequest = { + scopes: [scope], + }; + + // Generate PKCE code verifier and challenge + const generateCodeVerifier = () => { + const array = new Uint8Array(32); + window.crypto.getRandomValues(array); + return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join(""); + }; + + const codeVerifier = generateCodeVerifier(); + const codeChallenge = codeVerifier; + const state = Math.random().toString(36).substring(2, 15); + const authUrl = + `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + + `client_id=${appId}` + + `&response_type=code` + + `&redirect_uri=${encodeURIComponent(window.location.origin)}/authredirect` + + `&scope=${encodeURIComponent(scope)}` + + `&code_challenge=${codeChallenge}` + + `&code_challenge_method=plain` + + `&state=${state}` + + `&prompt=select_account`; + + // Open popup for authentication + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + authUrl, + "msalAuthPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Function to actually exchange the authorization code for tokens + const handleAuthorizationCode = async (code, receivedState) => { + // Verify the state parameter matches what we sent (security check) + if (receivedState !== state) { + const errorMessage = "State mismatch in auth response - possible CSRF attack"; + const error = { + errorCode: "state_mismatch", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + setAuthInProgress(false); + return; + } + try { + // Prepare the token request + const tokenRequest = { + grant_type: "authorization_code", + client_id: appId, + code: code, + redirect_uri: `${window.location.origin}/authredirect`, + code_verifier: codeVerifier, + }; + + // Make the token request through our API proxy to avoid origin header issues + const tokenResponse = await fetch(`/api/ExecTokenExchange`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tokenRequest, + tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + tenantId: appId, // Pass the tenant ID to retrieve the correct client secret + }), + }); + + // Parse the token response + const tokenData = await tokenResponse.json(); + + // Check if the response contains an error + if (tokenData.error) { + const error = { + errorCode: tokenData.error || "token_error", + errorMessage: + tokenData.error_description || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + setAuthInProgress(false); + return; + } + + if (tokenResponse.ok) { + // If we have a refresh token, store it + if (tokenData.refresh_token) { + try { + // Extract tid from access_token jwt base64 + const accessTokenParts = tokenData.access_token.split("."); + const accessTokenPayload = JSON.parse(atob(accessTokenParts[1] || "")); + tokenData.tid = accessTokenPayload.tid; + const refreshResponse = await fetch(`/api/ExecUpdateRefreshToken`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tenantId: tokenData.tid, + refreshtoken: tokenData.refresh_token, + tenantMode: tokenData.tenantMode, + allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement, + }), + }); + + if (!refreshResponse.ok) { + console.warn("Failed to store refresh token, but continuing with authentication"); + } + } catch (error) { + console.error("Failed to store refresh token:", error); + } + } + + handleTokenResponse(tokenData); + } else { + // Handle token error - display in error box instead of throwing + const error = { + errorCode: tokenData.error || "token_error", + errorMessage: + tokenData.error_description || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + } + } catch (error) { + const errorObj = { + errorCode: "token_exchange_error", + errorMessage: error.message || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(errorObj); + if (onAuthError) onAuthError(errorObj); + } finally { + // Close the popup window if it's still open + if (popup && !popup.closed) { + popup.close(); + } + + // Update UI state + setAuthInProgress(false); + } + }; + + // Monitor for the redirect with the authorization code + // This is what MSAL does internally + const checkPopupLocation = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(checkPopupLocation); + + // If authentication is still in progress when popup closes, it's an error + if (authInProgress) { + const errorMessage = "Authentication was cancelled. Please try again."; + const error = { + errorCode: "user_cancelled", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Ensure we're not showing any previous success state + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + } + + setAuthInProgress(false); + return; + } + + try { + // Try to access the popup location to check for the authorization code + const currentUrl = popup.location.href; + + // Check if the URL contains a code parameter (authorization code) + if (currentUrl.includes("code=") && currentUrl.includes("state=")) { + clearInterval(checkPopupLocation); + // Parse the URL to extract the code and state + const urlParams = new URLSearchParams(popup.location.search); + const code = urlParams.get("code"); + const receivedState = urlParams.get("state"); + + // Process the authorization code + handleAuthorizationCode(code, receivedState); + } + + // Check for error in the URL + if (currentUrl.includes("error=")) { + clearInterval(checkPopupLocation); + // Parse the URL to extract the error details + const urlParams = new URLSearchParams(popup.location.search); + const errorCode = urlParams.get("error"); + const errorDescription = urlParams.get("error_description"); + + // Set the error state + const error = { + errorCode: errorCode, + errorMessage: errorDescription || "Unknown authentication error", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Close the popup + popup.close(); + setAuthInProgress(false); + } + } catch (error) { + // This will throw an error when the popup is on a different domain + // due to cross-origin restrictions, which is normal during auth flow + // Just continue monitoring + } + }, 500); + + // Also monitor for popup closing as a fallback + }; + + // Auto-start device code retrieval if requested + useEffect(() => { + if ( + useDeviceCode && + autoStartDeviceLogon && + !codeRetrievalInProgress && + !deviceCodeInfo && + !tokens.accessToken && + appIdInfo?.data + ) { + retrieveDeviceCode(); + } + }, [ + useDeviceCode, + autoStartDeviceLogon, + codeRetrievalInProgress, + deviceCodeInfo, + tokens.accessToken, + appIdInfo?.data, + ]); + + return ( +
+ {!applicationId && + !appIdInfo.isLoading && + appIdInfo?.data && // Only check if data is available + !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( + appIdInfo?.data?.applicationId + ) && ( + + The Application ID is not valid. Please check your configuration. + + )} + + {showResults && ( + + {deviceCodeInfo ? ( + + Application Creation + + {authInProgress ? ( + <> + When asked to log onto an account, please use a{" "} + CIPP Service Account. Enter this code to authenticate:{" "} + + ) : ( + <> + Click the button below to authenticate. When asked to log onto an account, + please use a CIPP Service Account. You will need to enter this + code:{" "} + + )} + + + + {authInProgress ? ( + <> + If the popup was blocked or you closed it, you can also go to{" "} + microsoft.com/devicelogin manually and enter the code shown + above. + + ) : ( + <> + When you click the button below, a popup will open to{" "} + microsoft.com/devicelogin where you'll enter this code. + + )} + + + Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes + + + ) : tokens.accessToken ? ( + <> + {showSuccessAlert ? ( + + Authentication Successful + + You've successfully refreshed your token. The account you're using for + authentication is: {tokens.username} + + + Tenant ID: {tokens.tenantId} + {tokens.onmicrosoftDomain && ( + <> + {" "} + | Domain: {tokens.onmicrosoftDomain} + + )} + + + Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} + + + ) : null} + + {!isServiceAccount && ( + + Service Account Required + + CIPP requires a service account for authentication. The account you're using ( + {tokens.username}) does not appear to be a service account. + + + Please redo authentication using an account with "service" or "cipp" in the + username. + + + )} + + ) : authError ? ( + + + Authentication Error: {authError.errorCode} + + {authError.errorMessage} + + Time: {authError.timestamp} + + + + + + ) : null} + + )} + +
+ ); +}; diff --git a/src/components/CippComponents/CippAddEditTenantGroups.jsx b/src/components/CippComponents/CippAddEditTenantGroups.jsx index b07999089947..35696ef1fe52 100644 --- a/src/components/CippComponents/CippAddEditTenantGroups.jsx +++ b/src/components/CippComponents/CippAddEditTenantGroups.jsx @@ -1,6 +1,5 @@ -import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { Stack, Typography, Grid } from "@mui/material"; +import { Stack, Typography } from "@mui/material"; import CippFormSection from "/src/components/CippFormPages/CippFormSection"; import { CippFormTenantSelector } from "./CippFormTenantSelector"; diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 87a9fe56b689..6ec2daaad67b 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -1,13 +1,19 @@ -import { useRouter } from "next/router"; // Import Next.js router -import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Grid } from "@mui/material"; -import { Stack } from "@mui/system"; +import { useRouter } from "next/router"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + useMediaQuery, +} from "@mui/material"; +import { Stack, Grid } from "@mui/system"; import { CippApiResults } from "./CippApiResults"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; import CippFormComponent from "./CippFormComponent"; -import { useMediaQuery } from "@mui/material"; export const CippApiDialog = (props) => { const { @@ -25,7 +31,6 @@ export const CippApiDialog = (props) => { const [addedFieldData, setAddedFieldData] = useState({}); const [partialResults, setPartialResults] = useState([]); const [isFormSubmitted, setIsFormSubmitted] = useState(false); - const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); if (mdDown) { @@ -43,89 +48,66 @@ export const CippApiDialog = (props) => { url: "", waiting: false, queryKey: "", - relatedQueryKeys: relatedQueryKeys - ? relatedQueryKeys - : api.relatedQueryKeys - ? api.relatedQueryKeys - : title, + relatedQueryKeys: relatedQueryKeys ?? api.relatedQueryKeys ?? title, bulkRequest: api.multiPost === false, - onResult: (result) => { - setPartialResults((prevResults) => [...prevResults, result]); - }, + onResult: (result) => setPartialResults((prev) => [...prev, result]), }); const actionPostRequest = ApiPostCall({ urlFromData: true, - relatedQueryKeys: relatedQueryKeys - ? relatedQueryKeys - : api.relatedQueryKeys - ? api.relatedQueryKeys - : title, + relatedQueryKeys: relatedQueryKeys ?? api.relatedQueryKeys ?? title, bulkRequest: api.multiPost === false, onResult: (result) => { - setPartialResults((prevResults) => [...prevResults, result]); - if (api?.onSuccess) { - api.onSuccess(result); - } + setPartialResults((prev) => [...prev, result]); + api?.onSuccess?.(result); }, }); + const actionGetRequest = ApiGetCall({ ...getRequestInfo, - relatedQueryKeys: relatedQueryKeys - ? relatedQueryKeys - : api.relatedQueryKeys - ? api.relatedQueryKeys - : title, + relatedQueryKeys: relatedQueryKeys ?? api.relatedQueryKeys ?? title, bulkRequest: api.multiPost === false, onResult: (result) => { - setPartialResults((prevResults) => [...prevResults, result]); - if (api?.onSuccess) { - api.onSuccess(result); - } + setPartialResults((prev) => [...prev, result]); + api?.onSuccess?.(result); }, }); const processActionData = (dataObject, row, replacementBehaviour) => { - if (typeof api?.dataFunction === "function") { - return api.dataFunction(row); - } - var newData = {}; + if (typeof api?.dataFunction === "function") return api.dataFunction(row); + let newData = {}; if (api?.postEntireRow) { - newData = row; - } else { - Object.keys(dataObject).forEach((key) => { - const value = dataObject[key]; - if (typeof value === "string" && value.startsWith("!")) { - newData[key] = value.slice(1); - } else if (typeof value === "string") { - if (row[value] !== undefined) { - newData[key] = row[value]; - } else { - newData[key] = value; - } - } else if (typeof value === "boolean") { - newData[key] = value; - } else if (typeof value === "object" && value !== null) { - const processedValue = processActionData(value, row, replacementBehaviour); - if (replacementBehaviour !== "removeNulls" || Object.keys(processedValue).length > 0) { - newData[key] = processedValue; - } - } else if (replacementBehaviour !== "removeNulls") { - newData[key] = value; - } else if (row[value] !== undefined) { - newData[key] = row[value]; - } - }); + return row; } + + Object.keys(dataObject).forEach((key) => { + const value = dataObject[key]; + + if (typeof value === "string" && value.startsWith("!")) { + newData[key] = value.slice(1); + } else if (typeof value === "string") { + newData[key] = row[value] ?? value; + } else if (typeof value === "boolean") { + newData[key] = value; + } else if (typeof value === "object" && value !== null) { + const processedValue = processActionData(value, row, replacementBehaviour); + if (replacementBehaviour !== "removeNulls" || Object.keys(processedValue).length > 0) { + newData[key] = processedValue; + } + } else if (replacementBehaviour !== "removeNulls") { + newData[key] = value; + } + }); + return newData; }; + const tenantFilter = useSettings().currentTenant; const handleActionClick = (row, action, formData) => { setIsFormSubmitted(true); - if (action.multiPost === undefined) { - action.multiPost = false; - } + if (action.multiPost === undefined) action.multiPost = false; + if (api.customFunction) { action.customFunction(row, action, formData); createDialog.handleClose(); @@ -133,14 +115,15 @@ export const CippApiDialog = (props) => { } const commonData = { - tenantFilter: tenantFilter, + tenantFilter, ...formData, ...addedFieldData, }; const processedActionData = processActionData(action.data, row, action.replacementBehaviour); - if (Array.isArray(row) && action.multiPost === false) { - const arrayOfObjects = row.map((singleRow) => { + // MULTI ROW CASES + if (Array.isArray(row)) { + const arrayData = row.map((singleRow) => { const itemData = { ...commonData }; Object.keys(processedActionData).forEach((key) => { const rowValue = singleRow[processedActionData[key]]; @@ -148,58 +131,31 @@ export const CippApiDialog = (props) => { }); return itemData; }); - if (action.type === "POST") { - actionPostRequest.mutate({ - url: action.url, - bulkRequest: true, - data: arrayOfObjects, - }); - } else if (action.type === "GET") { - setGetRequestInfo({ - url: action.url, - waiting: true, - queryKey: Date.now(), - bulkRequest: true, - data: arrayOfObjects, - }); - } - return; - } - - if (Array.isArray(row) && action.multiPost === true) { - const singleArrayData = row.map((singleRow) => { - const itemData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = singleRow[processedActionData[key]]; - itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; - }); - return itemData; - }); + const payload = { + url: action.url, + bulkRequest: !action.multiPost, + data: arrayData, + }; if (action.type === "POST") { - actionPostRequest.mutate({ - url: action.url, - bulkRequest: false, - data: singleArrayData, - }); + actionPostRequest.mutate(payload); } else if (action.type === "GET") { setGetRequestInfo({ - url: action.url, + ...payload, waiting: true, queryKey: Date.now(), - bulkRequest: false, - data: singleArrayData, }); } + return; } - const finalData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = row[processedActionData[key]]; - finalData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; - }); + // ✅ FIXED: DIRECT MERGE INSTEAD OF CORRUPT TRANSFORMATION + const finalData = { + ...commonData, + ...processedActionData, + }; if (action.type === "POST") { actionPostRequest.mutate({ @@ -217,94 +173,71 @@ export const CippApiDialog = (props) => { }); } }; - //add a useEffect, when dialogAfterEffect exists, and the post or get request is successful, run the dialogAfterEffect function + useEffect(() => { if (dialogAfterEffect && (actionPostRequest.isSuccess || actionGetRequest.isSuccess)) { - dialogAfterEffect(actionPostRequest.data.data || actionGetRequest.data); + dialogAfterEffect(actionPostRequest.data?.data || actionGetRequest.data); } }, [actionPostRequest.isSuccess, actionGetRequest.isSuccess]); + const formHook = useForm(); const onSubmit = (data) => handleActionClick(row, api, data); const selectedType = api.type === "POST" ? actionPostRequest : actionGetRequest; useEffect(() => { if (api?.setDefaultValues && createDialog.open) { - fields.map((field) => { + fields.forEach((field) => { + const val = row[field.name]; if ( - ((typeof row[field.name] === "string" && field.type === "textField") || - (typeof row[field.name] === "boolean" && field.type === "switch")) && - row[field.name] !== undefined && - row[field.name] !== null && - row[field.name] !== "" + (typeof val === "string" && field.type === "textField") || + (typeof val === "boolean" && field.type === "switch") ) { - formHook.setValue(field.name, row[field.name]); - } else if (Array.isArray(row[field.name]) && field.type === "autoComplete") { - var values = []; - row[field.name].map((element) => { - if (element.label && element.value) { - values.push(element); - } else if (typeof element === "string" || typeof element === "number") { - values.push({ - label: element, - value: element, - }); - } - }); + formHook.setValue(field.name, val); + } else if (Array.isArray(val) && field.type === "autoComplete") { + const values = val + .map((el) => + el?.label && el?.value + ? el + : typeof el === "string" || typeof el === "number" + ? { label: el, value: el } + : null + ) + .filter(Boolean); formHook.setValue(field.name, values); - } else if ( - field.type === "autoComplete" && - row[field.name] !== "" && - (typeof row[field.name] === "string" || - (typeof row[field.name] === "object" && - row[field.name] !== undefined && - row[field.name] !== null)) - ) { - if (typeof row[field.name] === "string") { - formHook.setValue(field.name, { - label: row[field.name], - value: row[field.name], - }); - } else if ( - typeof row[field.name] === "object" && - row[field.name]?.label && - row[field.name]?.value - ) { - formHook.setValue(field.name, row[field.name]); - } + } else if (field.type === "autoComplete" && val) { + formHook.setValue( + field.name, + typeof val === "string" + ? { label: val, value: val } + : val.label && val.value + ? val + : undefined + ); } }); } }, [createDialog.open, api?.setDefaultValues]); - const getNestedValue = (obj, path) => { - return path + const getNestedValue = (obj, path) => + path .split(".") .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); - }; const [linkClicked, setLinkClicked] = useState(false); - - useEffect(() => { - setLinkClicked(false); - }, [api.link]); + useEffect(() => setLinkClicked(false), [api.link]); useEffect(() => { if (api.link && !linkClicked && row && Object.keys(row).length > 0) { const timeoutId = setTimeout(() => { - const linkWithRowData = api.link.replace(/\[([^\]]+)\]/g, (_, key) => { - return getNestedValue(row, key) || `[${key}]`; - }); - - if (linkWithRowData.startsWith("/") && !api?.external) { - // Internal link navigation - setLinkClicked(true); - router.push(linkWithRowData, undefined, { shallow: true }); - } else { - // External link navigation - setLinkClicked(true); - window.open(linkWithRowData, api.target || "_blank"); - } - }, 0); // Delay execution to the next event loop cycle + const linkWithData = api.link.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row, key) || `[${key}]` + ); + setLinkClicked(true); + if (linkWithData.startsWith("/") && !api?.external) + router.push(linkWithData, undefined, { shallow: true }); + else window.open(linkWithData, api.target || "_blank"); + }, 0); return () => clearTimeout(timeoutId); } @@ -312,41 +245,42 @@ export const CippApiDialog = (props) => { useEffect(() => { if (api.noConfirm && !api.link) { - formHook.handleSubmit(onSubmit)(); // Submits the form on mount - createDialog.handleClose(); // Closes the dialog after submitting + formHook.handleSubmit(onSubmit)(); + createDialog.handleClose(); } - }, [api.noConfirm, api.link]); // Run effect when noConfirm or link changes + }, [api.noConfirm, api.link]); const handleClose = () => { createDialog.handleClose(); setPartialResults([]); }; - var confirmText; + let confirmText; if (typeof api?.confirmText === "string") { if (!Array.isArray(row)) { - confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { - return getNestedValue(row, key) || `[${key}]`; - }); + confirmText = api.confirmText.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row, key) || `[${key}]` + ); } else if (row.length > 1) { confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, "the selected rows"); } else if (row.length === 1) { - confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { - return getNestedValue(row[0], key) || `[${key}]`; - }); + confirmText = api.confirmText.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row[0], key) || `[${key}]` + ); } } else { - // Handle JSX/Component confirmText const replaceTextInElement = (element) => { if (!element) return element; if (typeof element === "string") { - if (Array.isArray(row) && row.length > 1) { - return element.replace(/\[([^\]]+)\]/g, "the selected rows"); - } else if (Array.isArray(row) && row.length === 1) { - return element.replace( - /\[([^\]]+)\]/g, - (_, key) => getNestedValue(row[0], key) || `[${key}]` - ); + if (Array.isArray(row)) { + return row.length > 1 + ? element.replace(/\[([^\]]+)\]/g, "the selected rows") + : element.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row[0], key) || `[${key}]` + ); } return element.replace(/\[([^\]]+)\]/g, (_, key) => getNestedValue(row, key) || `[${key}]`); } @@ -370,29 +304,23 @@ export const CippApiDialog = (props) => { - {fields && - fields.map((fieldProps, index) => { - if (fieldProps?.api?.processFieldData) { - fieldProps.api.data = processActionData(fieldProps.api.data, row); - } - return ( - - - - ); - })} + {fields?.map((fieldProps, i) => ( + + + + ))} - +
+ )} +
+ + {showTestButton && ( + ({ + ...row, + text: "This is a test from Notification Settings", + }), + }} + /> + )} + + ); +}; + +export default CippNotificationForm; diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index 266c998619f7..6031f6497a27 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -1,8 +1,8 @@ -import { Drawer, Box, Grid, IconButton } from "@mui/material"; +import { Drawer, Box, IconButton } from "@mui/material"; import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../utils/get-cipp-translation"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; -import { useMediaQuery } from "@mui/system"; +import { useMediaQuery, Grid } from "@mui/system"; import CloseIcon from "@mui/icons-material/Close"; export const CippOffCanvas = (props) => { @@ -95,7 +95,7 @@ export const CippOffCanvas = (props) => { sx={{ overflowY: "auto", maxHeight: "100%", display: "flex", flexDirection: "column" }} > - + {extendedInfo.length > 0 && ( { /> )} - + {typeof children === "function" ? children(extendedData) : children} diff --git a/src/components/CippComponents/CippPermissionPreview.jsx b/src/components/CippComponents/CippPermissionPreview.jsx new file mode 100644 index 000000000000..9f2afeb69e06 --- /dev/null +++ b/src/components/CippComponents/CippPermissionPreview.jsx @@ -0,0 +1,381 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { + Alert, + Skeleton, + Stack, + Typography, + Box, + Paper, + List, + ListItem, + ListItemText, + Divider, + Tab, + Tabs, + Chip, + SvgIcon, +} from "@mui/material"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { ShieldCheckIcon } from "@heroicons/react/24/outline"; +import { CippCardTabPanel } from "./CippCardTabPanel"; + +const CippPermissionPreview = ({ + permissions, + title = "Permission Preview", + isLoading = false, + maxHeight = "100%", + showAppIds = true, +}) => { + const [selectedPermissionTab, setSelectedPermissionTab] = useState(0); + const [servicePrincipalDetails, setServicePrincipalDetails] = useState({}); + const [resourceIds, setResourceIds] = useState([]); + const [loadingDetails, setLoadingDetails] = useState(false); + + // Extract resource IDs from permissions object + useEffect(() => { + if (permissions && typeof permissions === "object") { + const ids = Object.keys(permissions); + setResourceIds(ids); + } + }, [permissions]); + + // Function to fetch individual service principal details + const fetchServicePrincipalDetails = useCallback(async (resourceId) => { + try { + const response = await fetch(`/api/ExecServicePrincipals?AppId=${resourceId}`); + const data = await response.json(); + + if (data?.Results) { + setServicePrincipalDetails((prev) => ({ + ...prev, + [resourceId]: data.Results, + })); + } + } catch (error) { + console.error(`Error fetching details for ${resourceId}:`, error); + } + }, []); + + // Fetch details for each resource ID + useEffect(() => { + const fetchAllDetails = async () => { + if (resourceIds.length > 0) { + setLoadingDetails(true); + const promises = resourceIds.map((id) => fetchServicePrincipalDetails(id)); + await Promise.all(promises); + setLoadingDetails(false); + } + }; + + fetchAllDetails(); + }, [resourceIds, fetchServicePrincipalDetails]); + + const handlePermissionTabChange = (event, newValue) => { + setSelectedPermissionTab(newValue); + }; + + // Function to get permission counts + const getPermissionCounts = (permissions) => { + if (!permissions) return { app: 0, delegated: 0 }; + + let appCount = 0; + let delegatedCount = 0; + + Object.entries(permissions).forEach(([resourceName, perms]) => { + if (perms.applicationPermissions) { + appCount += perms?.applicationPermissions?.length ?? 0; + } + if (perms.delegatedPermissions) { + delegatedCount += perms?.delegatedPermissions?.length ?? 0; + } + }); + + return { app: appCount, delegated: delegatedCount }; + }; + + // Helper to get the display name for a resource ID + const getResourceDisplayName = (resourceId) => { + const spDetails = servicePrincipalDetails[resourceId]; + return spDetails?.displayName || resourceId; + }; + + // Helper to get the appropriate permission description + const getPermissionDescription = (resourceId, permissionId, permissionType) => { + const spDetails = servicePrincipalDetails[resourceId]; + if (!spDetails) return null; + + if (permissionType === "application") { + const foundRole = spDetails.appRoles?.find((role) => role.id === permissionId); + return foundRole?.description || null; + } else { + const foundScope = spDetails.publishedPermissionScopes?.find( + (scope) => scope.id === permissionId + ); + return foundScope?.userConsentDescription || foundScope?.description || null; + } + }; + + // Better checks for permissions object to prevent rendering errors + if (isLoading || loadingDetails) { + + return ( + <> + {title} + + + ); + } + + if (!permissions) { + return ( + + Select a template with permissions to see what will be consented. + + ); + } + + // Ensure permissions is an object and has entries + if ( + typeof permissions !== "object" || + permissions === null || + Object.keys(permissions).length === 0 + ) { + return No permissions data available in this template.; + } + + return ( + + + {title} + + + + } + title="Application/Delegated Permissions" + /> + + + + + + + + + + + + + + {Object.entries(permissions).map(([resourceId, resourcePerms]) => { + const resourceName = getResourceDisplayName(resourceId); + const hasAppPermissions = + resourcePerms.applicationPermissions && + resourcePerms.applicationPermissions.length > 0; + const hasDelegatedPermissions = + resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0; + + return ( + + + + {resourceName} + {showAppIds && ( + + {resourceId} + + )} + + + {hasAppPermissions && ( + + + Application Permissions ({resourcePerms.applicationPermissions.length}) + + + {resourcePerms.applicationPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "application") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + )} + + {hasDelegatedPermissions && ( + + + Delegated Permissions ({resourcePerms.delegatedPermissions.length}) + + + {resourcePerms.delegatedPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "delegated") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + )} + + + ); + })} + + + + + + {Object.entries(permissions) + .filter( + ([_, perms]) => + perms.applicationPermissions && perms.applicationPermissions.length > 0 + ) + .map(([resourceId, resourcePerms]) => { + const resourceName = getResourceDisplayName(resourceId); + return ( + + + + {resourceName} + {showAppIds && ( + + {resourceId} + + )} + + + {resourcePerms.applicationPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "application") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + + ); + })} + {!Object.values(permissions).some( + (perms) => perms.applicationPermissions && perms.applicationPermissions.length > 0 + ) && No application permissions in this template.} + + + + + + {Object.entries(permissions) + .filter( + ([_, perms]) => perms.delegatedPermissions && perms.delegatedPermissions.length > 0 + ) + .map(([resourceId, resourcePerms]) => { + const resourceName = getResourceDisplayName(resourceId); + return ( + + + + {resourceName} + {showAppIds && ( + + {resourceId} + + )} + + + {resourcePerms.delegatedPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "delegated") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + + ); + })} + {!Object.values(permissions).some( + (perms) => perms.delegatedPermissions && perms.delegatedPermissions.length > 0 + ) && No delegated permissions in this template.} + + + + + ); +}; + +export default CippPermissionPreview; diff --git a/src/components/CippComponents/CippPropertyList.jsx b/src/components/CippComponents/CippPropertyList.jsx index 4c9fd3800820..e6b5fed8f1d0 100644 --- a/src/components/CippComponents/CippPropertyList.jsx +++ b/src/components/CippComponents/CippPropertyList.jsx @@ -53,7 +53,7 @@ export const CippPropertyList = (props) => { ) : ( // Two-column layout - { )) )} - + ) )} ); diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx index cb08993a8ea1..3a3c998c7c38 100644 --- a/src/components/CippComponents/CippSettingsSideBar.jsx +++ b/src/components/CippComponents/CippSettingsSideBar.jsx @@ -20,10 +20,8 @@ export const CippSettingsSideBar = (props) => { const { isDirty, isValid } = useFormState({ control: formcontrol.control }); const currentUser = ApiGetCall({ - url: "/.auth/me", + url: "/api/me", queryKey: "authmecipp", - staleTime: 120000, - refetchOnWindowFocus: true, }); const saveSettingsPost = ApiPostCall({ diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx index 37a3a9d44ccc..eb75f6cc1ad1 100644 --- a/src/components/CippComponents/CippTablePage.jsx +++ b/src/components/CippComponents/CippTablePage.jsx @@ -4,6 +4,7 @@ import Head from "next/head"; import { CippDataTable } from "../CippTable/CippDataTable"; import { useSettings } from "../../hooks/use-settings"; import { CippHead } from "./CippHead"; +import { useState } from "react"; export const CippTablePage = (props) => { const { @@ -23,10 +24,12 @@ export const CippTablePage = (props) => { queryKey, tableFilter, tenantInTitle = true, + filters, sx = { flexGrow: 1, py: 4 }, ...other } = props; const tenant = useSettings().currentTenant; + const [tableFilters] = useState(filters || []); return ( <> @@ -61,6 +64,13 @@ export const CippTablePage = (props) => { columns={columns} columnsFromApi={columnsFromApi} offCanvas={offCanvas} + filters={tableFilters} + initialState={{ + columnFilters: filters ? filters.map(filter => ({ + id: filter.id || filter.columnId, + value: filter.value + })) : [] + }} {...other} /> diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index fef71fa8ccb0..ebdbca9238bf 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -46,4 +46,7 @@ export const CippTranslations = { prohibitSendReceiveQuotaInBytes: "Quota", ClientId: "Client ID", html_url: "URL", + sendtoIntegration: "Send Notifications to Integration", + includeTenantId: "Include Tenant ID in Notifications", + logsToInclude: "Logs to Include in notifications", }; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index b443b4bcb856..5f6c190e474b 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -8,6 +8,7 @@ import { Email, ForwardToInbox, GroupAdd, + LockClock, LockOpen, LockPerson, LockReset, @@ -154,7 +155,10 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/ExecSetOoO", - data: { user: "userPrincipalName", AutoReplyState: "Disabled" }, + data: { + userId: "userPrincipalName", + AutoReplyState: { value: "Disabled" }, + }, confirmText: "Are you sure you want to disable the out of office?", multiPost: false, }, @@ -282,6 +286,29 @@ export const CippUserActions = () => { confirmText: "Are you sure you want to reset the password for this user?", multiPost: false, }, + { + label: "Set Password Never Expires", + type: "POST", + icon: , + url: "/api/ExecPasswordNeverExpires", + data: { userId: "id", userPrincipalName: "userPrincipalName" }, + fields: [ + { + type: "autoComplete", + name: "PasswordPolicy", + label: "Password Policy", + options: [ + { label: "Disable Password Expiration", value: "DisablePasswordExpiration" }, + { label: "Enable Password Expiration", value: "None" }, + ], + multiple: false, + creatable: false, + }, + ], + confirmText: + "Set Password Never Expires state for this user. If the password of the user is older than the set expiration date of the organization, the user will be prompted to change their password at their next login.", + multiPost: false, + }, { label: "Clear Immutable ID", type: "POST", @@ -308,7 +335,7 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/RemoveUser", - data: { ID: "id" }, + data: { ID: "id", userPrincipalName: "userPrincipalName" }, confirmText: "Are you sure you want to delete this user?", multiPost: false, }, diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index c7d6cc73993e..e86eaec59b74 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -5,15 +5,18 @@ import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormD import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; import countryList from "/src/data/countryList.json"; import { CippFormLicenseSelector } from "/src/components/CippComponents/CippFormLicenseSelector"; -import Grid from "@mui/material/Grid"; +import { Grid } from "@mui/system"; import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; import { useWatch } from "react-hook-form"; -import { useEffect } from "react"; +import { use, useEffect, useMemo } from "react"; +import { useRouter } from "next/router"; const CippAddEditUser = (props) => { const { formControl, userSettingsDefaults, formType = "add" } = props; const tenantDomain = useSettings().currentTenant; + const router = useRouter(); + const { userId } = router.query; const integrationSettings = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "ListExtensionsConfig", @@ -21,6 +24,36 @@ const CippAddEditUser = (props) => { refetchOnReconnect: false, }); + // Get all groups the is the user is a member of + const userGroups = ApiGetCall({ + url: `/api/ListUserGroups?userId=${userId}&tenantFilter=${tenantDomain}`, + queryKey: `User-${userId}-Groups-${tenantDomain}`, + refetchOnMount: false, + refetchOnReconnect: false, + waiting: !!userId, + }); + + // Get all groups for the tenant + const tenantGroups = ApiGetCall({ + url: `/api/ListGroups?tenantFilter=${tenantDomain}`, + queryKey: `ListGroups-${tenantDomain}`, + refetchOnMount: false, + refetchOnReconnect: false, + waiting: !!userId, + }); + + // Make new list of groups by removing userGroups from tenantGroups + const filteredTenantGroups = useMemo(() => { + if (tenantGroups.isSuccess && userGroups.isSuccess) { + const tenantGroupsList = tenantGroups?.data || []; + + return tenantGroupsList.filter( + (tenantGroup) => !userGroups?.data?.some((userGroup) => userGroup.id === tenantGroup.id) + ); + } + return []; + }, [tenantGroups.isSuccess, userGroups.isSuccess, tenantGroups.data, userGroups.data]); + const watcher = useWatch({ control: formControl.control }); useEffect(() => { //if watch.firstname changes, and watch.lastname changes, set displayname to firstname + lastname @@ -31,7 +64,7 @@ const CippAddEditUser = (props) => { return ( - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + - + { /> - + Settings - + { compareType="is" compareValue={true} > - + { - + { formControl={formControl} /> - + { formControl={formControl} /> - + {integrationSettings?.data?.Sherweb?.Enabled === true && ( @@ -151,7 +184,7 @@ const CippAddEditUser = (props) => { compareValue="(0 available)" labelCompare={true} > - + { compareType="is" compareValue={true} > - + This will Purchase a new Sherweb License for the user, according to the terms and conditions with Sherweb. When the license becomes available, CIPP will assign the license to this user. - + { )} - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + + + + + + + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { {userSettingsDefaults?.userAttributes ?.filter((attribute) => attribute.value !== "sponsor") .map((attribute, idx) => ( - + { ))} {/* Set Manager */} - + { /> {userSettingsDefaults?.userAttributes?.some((attribute) => attribute.value === "sponsor") && ( - + { /> )} - + { /> {formType === "edit" && ( - + ({ + label: tenantGroup.displayName, + value: tenantGroup.id, + addedFields: { + calculatedGroupType: tenantGroup.calculatedGroupType, + }, + }))} + formControl={formControl} + /> + + )} + {formType === "edit" && ( + + ({ + label: userGroups.DisplayName, + value: userGroups.id, + addedFields: { + calculatedGroupType: userGroups.calculatedGroupType, }, - }} + }))} formControl={formControl} /> )} {/* Schedule User Creation */} {formType === "add" && ( - + { compareType="is" compareValue={true} > - + { formControl={formControl} /> - + { return ( - + { fullWidth /> - + { fullWidth /> - + { }} /> - + { /> - + { /> - + { select={"id,userPrincipalName,displayName"} /> - + { compareType="is" compareValue="distribution" > - + { compareType="contains" compareValue="dynamic" > - + { const { formControl } = props; + // Debug the current form values, especially groupType + useEffect(() => { + const subscription = formControl.watch((value, { name, type }) => {}); + return () => subscription.unsubscribe(); + }, [formControl]); + return ( - + {/* Hidden field to store the template GUID when editing */} + + + { fullWidth /> - + { fullWidth /> - + { /> - + { { label: "Mail Enabled Security Group", value: "security" }, ]} /> + {/* Debug output */} +
Current groupType: {formControl.watch("groupType")}
{ compareType="is" compareValue="distribution" > - + { compareType="contains" compareValue="dynamic" > - + { const userSettingsDefaults = useSettings(); @@ -312,44 +314,80 @@ const CippExchangeSettingsForm = (props) => { multiple={false} formControl={formControl} /> - - - value ? true : "Select the permission level for the calendar", - }} - isFetching={isFetching || usersList.isFetching} - options={[ - { value: "Author", label: "Author" }, - { value: "Contributor", label: "Contributor" }, - { value: "Editor", label: "Editor" }, - { value: "Owner", label: "Owner" }, - { value: "NonEditingAuthor", label: "Non Editing Author" }, - { value: "PublishingAuthor", label: "Publishing Author" }, - { value: "PublishingEditor", label: "Publishing Editor" }, - { value: "Reviewer", label: "Reviewer" }, - { value: "LimitedDetails", label: "Limited Details" }, - { value: "AvailabilityOnly", label: "Availability Only" }, - ]} - multiple={false} - formControl={formControl} - /> + + + value ? true : "Select the permission level for the calendar", + }} + isFetching={isFetching || usersList.isFetching} + options={[ + { value: "Author", label: "Author" }, + { value: "Contributor", label: "Contributor" }, + { value: "Editor", label: "Editor" }, + { value: "Owner", label: "Owner" }, + { value: "NonEditingAuthor", label: "Non Editing Author" }, + { value: "PublishingAuthor", label: "Publishing Author" }, + { value: "PublishingEditor", label: "Publishing Editor" }, + { value: "Reviewer", label: "Reviewer" }, + { value: "LimitedDetails", label: "Limited Details" }, + { value: "AvailabilityOnly", label: "Availability Only" }, + ]} + multiple={false} + formControl={formControl} + /> + + {(() => { + const permissionLevel = useWatch({ + control: formControl.control, + name: "calendar.Permissions" + }); + const isEditor = permissionLevel?.value === "Editor"; + + // Use useEffect to handle the switch value reset + useEffect(() => { + if (!isEditor) { + formControl.setValue("calendar.CanViewPrivateItems", false); + } + }, [isEditor, formControl]); + + return ( + + + + + + ); + })()} + + + diff --git a/src/components/CippFormPages/CippFormPage.jsx b/src/components/CippFormPages/CippFormPage.jsx index d22877dfb139..448eb0604498 100644 --- a/src/components/CippFormPages/CippFormPage.jsx +++ b/src/components/CippFormPages/CippFormPage.jsx @@ -11,7 +11,6 @@ import { CardActions, } from "@mui/material"; import ArrowLeftIcon from "@mui/icons-material/ArrowLeft"; -import Head from "next/head"; import { ApiPostCall } from "../../api/ApiCall"; import { CippApiResults } from "../CippComponents/CippApiResults"; import { useEffect } from "react"; @@ -71,6 +70,11 @@ const CippFormPage = (props) => { }, [postCall.isSuccess]); const handleSubmit = () => { + formControl.trigger(); + // Check if the form is valid before proceeding + if (!isValid) { + return; + } const values = customDataformatter ? customDataformatter(formControl.getValues()) : formControl.getValues(); diff --git a/src/components/CippFormPages/CippFormSkeleton.jsx b/src/components/CippFormPages/CippFormSkeleton.jsx index 086b6b09c378..77a04daf9b0d 100644 --- a/src/components/CippFormPages/CippFormSkeleton.jsx +++ b/src/components/CippFormPages/CippFormSkeleton.jsx @@ -1,4 +1,6 @@ -import { Box, Grid, Skeleton } from "@mui/material"; +import { Box, Skeleton } from "@mui/material"; + +import { Grid } from "@mui/system"; const CippFormSkeleton = ({ layout }) => { return ( @@ -6,7 +8,7 @@ const CippFormSkeleton = ({ layout }) => { {layout.map((columns, rowIndex) => ( {Array.from({ length: columns }).map((_, columnIndex) => ( - + ))} diff --git a/src/components/CippFormPages/CippInviteGuest.jsx b/src/components/CippFormPages/CippInviteGuest.jsx index 4c687c811083..6e3f597fcbfb 100644 --- a/src/components/CippFormPages/CippInviteGuest.jsx +++ b/src/components/CippFormPages/CippInviteGuest.jsx @@ -1,11 +1,12 @@ -import { Grid } from "@mui/material"; +import "@mui/material"; +import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; const CippInviteUser = (props) => { const { formControl, userSettingsDefaults } = props; return ( - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + ( 4, and add spacing between the top and bottom items diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index d49b2f069254..580ad9116d0b 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Box, Button, Divider, Grid, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Box, Button, Divider, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; @@ -146,7 +147,7 @@ const CippSchedulerForm = (props) => { {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( )} - + { /> - + { /> - + { }} /> - + { }} /> - + { /> {selectedCommand?.addedFields?.Synopsis && ( - + PowerShell Command: @@ -236,8 +237,7 @@ const CippSchedulerForm = (props) => { > {param.Type === "System.Boolean" || @@ -279,10 +279,10 @@ const CippSchedulerForm = (props) => { ))} - + - + { compareValue={true} formControl={formControl} > - + { /> - + { ]} /> - + + + + { + setOffcanvasVisible(false); + }} + > + + + {`${cat}.${obj}`} + + + Listed below are the available API endpoints based on permission level, ReadWrite + level includes endpoints under Read. + + {[apiPermissions[cat][obj]].map((permissions, key) => { + var sections = Object.keys(permissions).map((type) => { + var items = []; + for (var api in permissions[type]) { + items.push({ heading: "", content: permissions[type][api] }); + } + return ( + + {type} + + {items.map((item, idx) => ( + + {item.content} + + ))} + + + ); + }); + return sections; + })} + + + + ); + }; + + return ( + <> + + + + {!selectedRole && ( + + )} + {selectedRole && isBaseRole && ["admin", "superadmin"].includes(selectedRole) && ( + }> + This is a highly privileged role and overrides any custom role restrictions. + + )} + {cippApiRoleSelected && ( + + This is the default role for all API clients in the CIPP-API integration. If you + would like different permissions for specific applications, create a role per + application and select it from the CIPP-API integrations page. + + )} + + + {!isBaseRole && ( + <> + + + {allTenantSelected && blockedTenants?.length == 0 && ( + + All tenants selected, no tenant restrictions will be applied unless blocked + tenants are specified. + + )} + + {allTenantSelected && ( + + + + )} + + )} + {apiPermissionFetching && } + {apiPermissionSuccess && ( + <> + API Permissions + {!isBaseRole && ( + + Set All Permissions + + + + + + )} + + <> + {Object.keys(apiPermissions) + .sort() + .map((cat, catIndex) => ( + + }>{cat} + + {Object.keys(apiPermissions[cat]) + .sort() + .map((obj, index) => { + const readOnly = baseRolePermissions?.[cat] ? true : false; + return ( + + + + ); + })} + + + ))} + + + + )} + + + + {selectedEntraGroup && ( + + This role will be assigned to the Entra Group:{" "} + {selectedEntraGroup.label} + + )} + {selectedTenant?.length > 0 && ( + <> +
Allowed Tenants
+
    + {selectedTenant.map((tenant, idx) => ( +
  • {tenant?.label}
  • + ))} +
+ + )} + {blockedTenants?.length > 0 && ( + <> +
Blocked Tenants
+
    + {blockedTenants.map((tenant, idx) => ( +
  • {tenant?.label}
  • + ))} +
+ + )} + {selectedPermissions && apiPermissionSuccess && ( + <> +
Selected Permissions
+
    + {selectedPermissions && + Object.keys(selectedPermissions) + ?.sort() + .map((cat, idx) => ( + <> + {selectedPermissions?.[cat] && + typeof selectedPermissions[cat] === "string" && + !selectedPermissions[cat]?.includes("None") && ( +
  • {selectedPermissions[cat]}
  • + )} + + ))} +
+ + )} +
+
+ + + + + + + ); +}; + +export default CippRoleAddEdit; diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx new file mode 100644 index 000000000000..15766897d4f4 --- /dev/null +++ b/src/components/CippSettings/CippRoles.jsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Box, Button, SvgIcon } from "@mui/material"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import NextLink from "next/link"; +import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { Stack } from "@mui/system"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; + +const CippRoles = () => { + const actions = [ + { + label: "Edit", + icon: ( + + + + ), + link: "/cipp/super-admin/cipp-roles/edit?role=[RoleName]", + }, + { + label: "Delete", + icon: ( + + + + ), + confirmText: "Are you sure you want to delete this custom role?", + url: "/api/ExecCustomRole", + type: "POST", + data: { + Action: "Delete", + RoleName: "RoleName", + }, + condition: (row) => row?.Type === "Custom", + relatedQueryKeys: ["customRoleList"], + }, + ]; + + const offCanvas = { + children: (data) => { + const includeProps = ["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]; + const keys = includeProps.filter((key) => Object.keys(data).includes(key)); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + + if (data["Permissions"] && Object.keys(data["Permissions"]).length > 0) { + properties.push({ + label: "Permissions", + value: ( + + {Object.keys(data["Permissions"]) + .sort() + .map((permission, idx) => ( + + + + ))} + + ), + }); + } + + return ( + + ); + }, + }; + + return ( + + + + + } + component={NextLink} + href="/cipp/super-admin/cipp-roles/add" + > + Add Role + + } + api={{ + url: "/api/ListCustomRole", + }} + queryKey="customRoleTable" + simpleColumns={["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]} + offCanvas={offCanvas} + /> + + ); +}; + +export default CippRoles; diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 8037de3b2e2b..4048981e6cd5 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -9,7 +9,6 @@ import { SvgIcon, Collapse, Divider, - Grid, Tooltip, Chip, TextField, @@ -26,6 +25,7 @@ import { Close, FilterAlt, } from "@mui/icons-material"; +import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useWatch } from "react-hook-form"; import _ from "lodash"; @@ -37,6 +37,7 @@ import Intune from "../../icons/iconly/bulk/intune"; import GDAPRoles from "/src/data/GDAPRoles"; import timezoneList from "/src/data/timezoneList"; import standards from "/src/data/standards.json"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; const getAvailableActions = (disabledFeatures) => { const allActions = [ @@ -68,7 +69,7 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl }) } return ( - + { + // Skip validation for components with conditions + if (component.condition) { + const conditionField = `${standardName}.${component.condition.field}`; + const conditionValue = _.get(watchedValues, conditionField); + const compareType = component.condition.compareType || "is"; + const compareValue = component.condition.compareValue; + const propertyName = component.condition.propertyName || "value"; + + // Check if condition is met based on the compareType + let conditionMet = false; + if (propertyName === "value") { + switch (compareType) { + case "is": + conditionMet = _.isEqual(conditionValue, compareValue); + break; + case "isNot": + conditionMet = !_.isEqual(conditionValue, compareValue); + break; + // Add other compareType cases as needed + default: + conditionMet = false; + } + } else if (Array.isArray(conditionValue)) { + // Handle array values with propertyName + switch (compareType) { + case "valueEq": + conditionMet = conditionValue.some( + (item) => item?.[propertyName] === compareValue + ); + break; + // Add other compareType cases for arrays as needed + default: + conditionMet = false; + } + } + + // If condition is not met, we don't need to validate this field + if (!conditionMet) { + return true; + } + } + const isRequired = component.required !== false && component.type !== "switch"; if (!isRequired) return true; return !!_.get(watchedValues, `${standardName}.${component.name}`); @@ -370,7 +413,7 @@ const CippStandardAccordion = ({ /> )} - + {standard.helpText} @@ -394,9 +437,11 @@ const CippStandardAccordion = ({ {isConfigured ? "Configured" : "Unconfigured"} - handleRemoveStandard(standardName)}> - - + + handleRemoveStandard(standardName)}> + + + handleAccordionToggle(standardName)}> - + {hasAddedComponents && ( - + - {standard.addedComponent?.map((component, idx) => ( - - ))} + {standard.addedComponent?.map((component, idx) => + component?.condition ? ( + + + + ) : ( + + ) + )} )} diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index cb9f571218c3..bdf407da2bf4 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -5,7 +5,6 @@ import { DialogContent, DialogTitle, TextField, - Grid, Card, CardContent, Typography, @@ -16,6 +15,7 @@ import { Button, IconButton, } from "@mui/material"; +import { Grid } from "@mui/system"; import { Add } from "@mui/icons-material"; import { useState, useCallback } from "react"; import { debounce } from "lodash"; @@ -59,6 +59,7 @@ const CippStandardDialog = ({ open={dialogOpen} onClose={handleCloseDialog} maxWidth="xxl" + fullWidth PaperProps={{ sx: { minWidth: "720px", @@ -87,7 +88,7 @@ const CippStandardDialog = ({ ) : ( Object.keys(categories).map((category) => filterStandards(categories[category]).map((standard) => ( - + - {watchForm.tenantFilter?.some((tenant) => tenant.value === "AllTenants") && ( + {watchForm.tenantFilter?.some((tenant) => tenant.value === "AllTenants" || tenant.type === "Group" ) && ( <> setActionMenuAnchor(event.currentTarget); const handleActionMenuClose = () => setActionMenuAnchor(null); + const getBulkActions = (actions) => { + return actions?.filter((action) => !action.link && !action?.hideBulk) || []; + }; + useEffect(() => { //if usedData changes, deselect all rows table.toggleAllRowsSelected(false); @@ -365,6 +369,7 @@ export const CIPPTableToptoolbar = ({ {api?.url === "/api/ListGraphRequest" && ( { filterPopover.handleClose(); setFilterCanvasVisible(true); @@ -448,9 +453,7 @@ export const CIPPTableToptoolbar = ({ - {mdDown && ( - - )} + {mdDown && } { //add a little icon with how many rows are selected @@ -487,7 +490,7 @@ export const CIPPTableToptoolbar = ({ )} - {actions && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && ( + {actions && getBulkActions(actions).length > 0 && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && ( <> - } - > - - Click the button below and enter the provided code. This creates the CIPP - Application Registration in your tenant that allows you to access the Graph API. - Login using your CIPP Service Account. - - {startSetupApi.isLoading ? ( - - ) : ( - - )} -
- )} - {currentStepState >= 2 && ( - - Step 2: Approve Permissions - - {currentStepState <= 2 ? ( - - ) : ( - - - - )} - - - } - CardButton={ - - } - > - - Step 2: Approvals Required - - - Please open the link below and provide the required approval, this allows the app - specific permissions shown in the next screen. Login using your CIPP Service - Account. - - - )} - - {/* Final Step 4 Card */} - {currentStepState >= 4 && } - - - - - - - - - )} - - {values.selectedOption === "UpdateTokens" && ( - - Update Tokens - - {appId.isLoading ? ( - - ) : ( - - - - )} - - - } - CardButton={ - <> - - - {!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( - appId?.data?.applicationId - ) && ( - - The Application ID is not valid. Please return to the first page of the SAM - wizard and use the Manual . - - )} - - } - > - - Click the button below to refresh your token. - - {formControl.setValue("noSubmitButton", true)} - - - )} - - {values.selectedOption === "Manual" && ( - <> - {formControl.setValue("setKeys", true)} - - You may enter your secrets below. Leave fields blank to retain existing values. - - { - const guidRegex = - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; - return value === "" || guidRegex.test(value) || "Invalid Tenant ID"; - }, - }} - /> - { - const guidRegex = - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; - return value === "" || guidRegex.test(value) || "Invalid Application ID"; - }, - }} - /> - { - const secretRegex = /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/; - return ( - value === "" || - secretRegex.test(value) || - "This should be the secret value, not the secret ID" - ); - }, - }} - /> - { - const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; - return value === "" || jwtRegex.test(value) || "Invalid Refresh Token"; - }, - }} - /> - - )} - - - - ); -}; diff --git a/src/components/CippWizard/CIPPDeploymentStep.jsx b/src/components/CippWizard/CIPPDeploymentStep.jsx new file mode 100644 index 000000000000..7a6553b9323c --- /dev/null +++ b/src/components/CippWizard/CIPPDeploymentStep.jsx @@ -0,0 +1,101 @@ +import { useEffect } from "react"; +import { Stack, Typography } from "@mui/material"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CIPPDeploymentUpdateTokens } from "./CIPPDeploymentUpdateTokens"; + +export const CippDeploymentStep = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + const values = formControl.getValues(); + + // Use useEffect to set form values instead of doing it during render + useEffect(() => { + if (values.selectedOption === "Manual") { + formControl.setValue("setKeys", true); + } + }, [values.selectedOption, formControl]); + + return ( + + + {values.selectedOption === "UpdateTokens" && ( + + )} + + {values.selectedOption === "Manual" && ( + <> + + You may enter your secrets below. Leave fields blank to retain existing values. + + { + const guidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return value === "" || guidRegex.test(value) || "Invalid Tenant ID"; + }, + }} + /> + { + const guidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return value === "" || guidRegex.test(value) || "Invalid Application ID"; + }, + }} + /> + { + const secretRegex = + /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/; + return ( + value === "" || + secretRegex.test(value) || + "This should be the secret value, not the secret ID" + ); + }, + }} + /> + { + const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; + return value === "" || jwtRegex.test(value) || "Invalid Refresh Token"; + }, + }} + /> + + )} + + + + ); +}; diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx new file mode 100644 index 000000000000..50fef63317f2 --- /dev/null +++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material"; +import { CheckCircle } from "@mui/icons-material"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiGetCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; + +export const CIPPDeploymentUpdateTokens = ({ formControl }) => { + const [tokens, setTokens] = useState(null); + + // Get application ID information for the card header + const appId = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle successful authentication + const handleAuthSuccess = (tokenData) => { + setTokens(tokenData); + }; + + return ( + + + Update Tokens + + {appId.isLoading ? ( + + ) : ( + + + + )} + + + } + CardButton={ + + } + > + + Click the button to refresh the Graph token for your tenants using popup authentication. + This method opens a popup window where you can sign in to your Microsoft account. + + + + + ); +}; + +export default CIPPDeploymentUpdateTokens; diff --git a/src/components/CippWizard/CippAlertsStep.jsx b/src/components/CippWizard/CippAlertsStep.jsx new file mode 100644 index 000000000000..8e8e7b832ccb --- /dev/null +++ b/src/components/CippWizard/CippAlertsStep.jsx @@ -0,0 +1,94 @@ +import { Alert, Stack, Typography, Box } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippFormTenantSelector } from "../CippComponents/CippFormTenantSelector"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; +import alertList from "../../data/alerts.json"; + +export const CippAlertsStep = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + const postExecutionOptions = [ + { label: "Webhook", value: "Webhook" }, + { label: "Email", value: "Email" }, + { label: "PSA", value: "PSA" }, + ]; + + const recurrenceOptions = [ + { value: "30m", label: "Every 30 minutes" }, + { value: "1h", label: "Every hour" }, + { value: "4h", label: "Every 4 hours" }, + { value: "1d", label: "Every 1 day" }, + { value: "7d", label: "Every 7 days" }, + { value: "30d", label: "Every 30 days" }, + { value: "365d", label: "Every 365 days" }, + ]; + + return ( + + + Almost done + + + There's a couple more things that you can configure outside of the wizard, let's list + some of them; + +
    +
  • + CIPP has the ability to send alerts to your PSA, Webhook or Email. You can configure + these settings under > Tenant Administration > Alert Configuration. +
  • +
+
    +
  • + If you imported baselines, or want to set tenants to your own baseline, you should + check out our standards under these settings under > Tenant Administration > + Standards. +
  • +
+
    +
  • + If you want to use our integrations, you should set these up under > CIPP > + Integrations. Some examples are CSP integrations, Password Pusher, PSA, and more. +
  • +
+
    +
  • + Adding more users to CIPP? you can do this via CIPP > Advanced > Super Admin. +
  • +
+
    +
  • + You can deploy Windows Applications too, directly using intune. We have Chocolately, + WinGet, and RMM apps under > Intune > Applications. Some examples are CSP + integrations, Password Pusher, PSA, and more. +
  • +
+
    +
  • + Tenants can be grouped, and you can implement custom variables for your tenants under + WinGet, and RMM apps under Tenant Administrator > Administration > Tenants. +
  • +
+
    +
  • + Have an enterprise app you want to deploy? Check out our tools{" "} + section. This menu also contains useful things such as our geo-ip lookup, and more. +
  • +
+
+
+ + +
+ ); +}; + +export default CippAlertsStep; diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx new file mode 100644 index 000000000000..0343e8a33f1c --- /dev/null +++ b/src/components/CippWizard/CippBaselinesStep.jsx @@ -0,0 +1,105 @@ +import { Alert, Stack, Typography, FormControl, FormLabel, Box } from "@mui/material"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; + +export const CippBaselinesStep = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + return ( + + + + + Baselines are template configurations that can be used as examples for setting up your + environment. Don't want to configure these yet? No problem! You can find the templates + at Tools - Community Repositories + + + Downloading these baselines will create templates in your CIPP instance. These templates + won't make any changes to your environment, but can be used as examples on how to setup + environments. Each template library contains multiple templates, +
    +
  • + CIPP Templates by CyberDrain contain several example standards, including low, + medium, and high priority standards +
  • +
  • + JoeyV's Conditional Access Baseline contains a Microsoft approved baseline for + Conditional Access, following the Microsoft best practices. +
  • +
  • + OpenIntuneBaseline contains Intune templates, the baseline is a community driven + baseline for Intune, based on CIS, NIST, and more benchmarks. It's considered the + leading baseline for Intune. +
  • +
+
+
+ + + Baseline Configuration + + + + + + + + + Select baselines to download: + + `${option.Name} (${option.Owner})`, + valueField: "FullName", + addedFields: { + templateRepoBranch: "main", + }, + }} + multiple={true} + placeholder="Select one or more baselines" + /> + + +
+ + +
+ ); +}; + +export default CippBaselinesStep; diff --git a/src/components/CippWizard/CippCAForm.jsx b/src/components/CippWizard/CippCAForm.jsx index b99d69c8e55c..8f82bf27e792 100644 --- a/src/components/CippWizard/CippCAForm.jsx +++ b/src/components/CippWizard/CippCAForm.jsx @@ -1,4 +1,5 @@ -import { Grid, Stack } from "@mui/material"; +import { Stack } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippWizardStepButtons } from "./CippWizardStepButtons"; import CippJsonView from "../CippFormPages/CippJSONView"; import CippFormComponent from "../CippComponents/CippFormComponent"; diff --git a/src/components/CippWizard/CippIntunePolicy.jsx b/src/components/CippWizard/CippIntunePolicy.jsx index e21491751546..e79dd9c07e6d 100644 --- a/src/components/CippWizard/CippIntunePolicy.jsx +++ b/src/components/CippWizard/CippIntunePolicy.jsx @@ -1,4 +1,5 @@ -import { Grid, Stack } from "@mui/material"; +import { Stack } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippWizardStepButtons } from "./CippWizardStepButtons"; import CippJsonView from "../CippFormPages/CippJSONView"; import CippFormComponent from "../CippComponents/CippFormComponent"; @@ -58,7 +59,7 @@ export const CippIntunePolicy = (props) => { /> - + { compareType="is" compareValue="customGroup" > - + { return null; } return uniquePlaceholders.map((placeholder) => ( - + {selectedTenants.map((tenant, idx) => ( { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + return ( + + + Notification Settings + + Configure your notification settings. These settings will determine how you receive alerts + from CIPP. You can test your configuration using the "Send Test Alert" button. Don't want + to setup notifications yet? You can skip this step and configure it later via Application + Settings - Notifications + + {/* Use the reusable notification form component */} + + + + {/* Use the wizard step buttons for navigation */} + + + ); +}; + +export default CippNotificationsStep; diff --git a/src/components/CippWizard/CippPSACredentialsStep.js b/src/components/CippWizard/CippPSACredentialsStep.jsx similarity index 100% rename from src/components/CippWizard/CippPSACredentialsStep.js rename to src/components/CippWizard/CippPSACredentialsStep.jsx diff --git a/src/components/CippWizard/CippPSASyncOptions.js b/src/components/CippWizard/CippPSASyncOptions.jsx similarity index 100% rename from src/components/CippWizard/CippPSASyncOptions.js rename to src/components/CippWizard/CippPSASyncOptions.jsx diff --git a/src/components/CippWizard/CippSAMDeploy.jsx b/src/components/CippWizard/CippSAMDeploy.jsx new file mode 100644 index 000000000000..8f04afbd5336 --- /dev/null +++ b/src/components/CippWizard/CippSAMDeploy.jsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from "react"; +import { Alert, Stack, Box, CircularProgress, Link } from "@mui/material"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; + +export const CippSAMDeploy = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + const [authStatus, setAuthStatus] = useState({ + success: false, + error: null, + loading: false, + }); + + // Block next step until SAM app is created + formControl.register("SAMWizard", { + required: true, + }); + + // Set SAMWizard = true if auth is successful + useEffect(() => { + if (authStatus.success) { + formControl.setValue("SAMWizard", true); + formControl.trigger("SAMWizard"); + } + }, [authStatus, formControl]); + + const createSamApp = ApiPostCall({ urlfromdata: true }); + + const handleAuthSuccess = (tokenData) => { + setAuthStatus({ + success: false, + error: null, + loading: true, + }); + + createSamApp.mutate({ + url: "/api/ExecCreateSamApp", + data: { access_token: tokenData.accessToken }, + }); + }; + + const handleAuthError = (error) => { + setAuthStatus({ + success: false, + error: error.errorMessage || "Authentication failed", + loading: false, + }); + }; + + useEffect(() => { + if (createSamApp.isSuccess && authStatus.loading && createSamApp.data) { + const data = createSamApp.data?.data; + if (data.severity === "error") { + setAuthStatus({ + success: false, + error: data.message || "Failed to create SAM application", + loading: false, + }); + } else if (data.severity === "success") { + setAuthStatus({ + success: true, + error: null, + loading: false, + }); + } + } + }, [createSamApp, authStatus]); + + useEffect(() => { + if (createSamApp.isError && authStatus.loading) { + setAuthStatus({ + success: false, + error: "An error occurred while creating the SAM application", + loading: false, + }); + } + }, [createSamApp, authStatus]); + + return ( + + + To run this setup you will need the following prerequisites: +
  • + A CIPP Service Account. For more information on how to create a service account, click{" "} + + here + +
  • +
  • (Temporary) Global Administrator permissions for the CIPP Service Account
  • +
  • + Multi-factor authentication enabled for the CIPP Service Account, with no trusted + locations or other exclusions. +
  • +
    + + {authStatus.error && ( + + {authStatus.error} + + )} + + + + + + + + +
    + ); +}; + +export default CippSAMDeploy; diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx new file mode 100644 index 000000000000..8f8683af405e --- /dev/null +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -0,0 +1,130 @@ +import { useEffect } from "react"; +import { Stack, Box, Typography, Link } from "@mui/material"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippTenantTable } from "./CippTenantTable"; + +export const CippTenantModeDeploy = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + + formControl.register("GDAPAuth", { + required: true, + }); + + const updateRefreshToken = ApiPostCall({ urlfromdata: true }); + const addTenant = ApiPostCall({ urlfromdata: true }); + + useEffect(() => { + if (updateRefreshToken.isSuccess) { + formControl.setValue("GDAPAuth", true); + formControl.trigger("GDAPAuth"); + } + if (addTenant.isSuccess) { + // Reset the form control for the next tenant addition + formControl.setValue("GDAPAuth", true); + formControl.trigger("GDAPAuth"); + } + }, [updateRefreshToken.isSuccess, formControl, addTenant.isSuccess]); + + return ( + + + + {/* Partner Tenant (GDAP) */} + + + Partner Tenant + + + Using GDAP is recommended for CIPP, however you can also authenticate to individual + tenants. It is still highly recommended to connect to your partner tenant first, even if + you are not a Microsoft CSP. This allows CIPP to send notifications, perform permission + checks, and update permissions when required. + + + Please remember to log onto a service account dedicated for CIPP. More info? Check out the{" "} + + service account documentation + + . + + + + + { + const updatedTokenData = { + ...tokenData, + tenantMode: "GDAP", + }; + updateRefreshToken.mutate({ + url: "/api/ExecUpdateRefreshToken", + data: updatedTokenData, + }); + }} + buttonText="Connect to Partner Tenant (Recommended)" + showSuccessAlert={false} + /> + + + + + {/* Per-Tenant */} + + + Per-Tenant Authentication + + + Click the button below to connect to individual tenants. You can authenticate to multiple + tenants by repeating this step for each tenant you want to add. Accidentally added the + wrong tenant? Use the table below to remove it. + + + + + { + const updatedTokenData = { + ...tokenData, + tenantMode: "perTenant", + }; + addTenant.mutate({ + url: "/api/ExecAddTenant", + data: updatedTokenData, + }); + }} + buttonText="Connect to Separate Tenants" + showSuccessAlert={false} + /> + + + + + + + + + ); +}; + +export default CippTenantModeDeploy; diff --git a/src/components/CippWizard/CippTenantTable.jsx b/src/components/CippWizard/CippTenantTable.jsx new file mode 100644 index 000000000000..10d7490babdc --- /dev/null +++ b/src/components/CippWizard/CippTenantTable.jsx @@ -0,0 +1,168 @@ +import React from "react"; +import { Button, SvgIcon } from "@mui/material"; +import { CippTablePage } from "../CippComponents/CippTablePage.jsx"; +import { Sync, Block, PlayArrow, RestartAlt, Delete, Add } from "@mui/icons-material"; +import { useDialog } from "../../hooks/use-dialog"; +import { CippApiDialog } from "../CippComponents/CippApiDialog"; + +export const CippTenantTable = ({ + title = "Tenants", + tenantInTitle = false, + customColumns = null, + customFilters = null, + showCardButton = true, + showTenantSelector = true, + showAllTenantsSelector = true, + onRefreshButtonClick = null, +}) => { + 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 [displayName]?", + 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 [displayName]?", + 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 [displayName]?", + 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 [displayName]? (This will delete the Service Principal and re-add it.)", + multiPost: false, + condition: (row) => + row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant", + }, + { + label: "Remove Tenant", + type: "POST", + url: `/api/ExecRemoveTenant`, + icon: , + data: { TenantID: "customerId" }, + confirmText: + "Are you sure you want to remove [displayName]? If this is a Direct Tenant, this will no longer be accessible until you add it via the Setup Wizard.", + multiPost: false, + condition: (row) => row.displayName !== "*Partner Tenant", + }, + ]; + + // Offcanvas details + const offCanvas = { + extendedInfoFields: [ + "displayName", + "defaultDomainName", + "delegatedPrivilegeStatus", + "Excluded", + "ExcludeDate", + "ExcludeUser", + ], + actions: actions, + }; + + // Columns for the table + const columns = customColumns || [ + "displayName", // Tenant Name + "defaultDomainName", // Default Domain + "delegatedPrivilegeStatus", // Delegated Privilege Status + "Excluded", // Excluded Status + "ExcludeDate", // Exclude Date + "ExcludeUser", // Exclude User + ]; + + // Default filters + const defaultFilters = [ + { + filterName: "Included tenants", + value: [{ id: "Excluded", value: "No" }], + type: "column", + }, + { + filterName: "Excluded tenants", + value: [{ id: "Excluded", value: "Yes" }], + type: "column", + }, + ]; + + const filters = customFilters || defaultFilters; + + return ( + <> + + + + + Force Refresh + + ) : null + } + tenantInTitle={tenantInTitle} + apiUrl="/api/ExecExcludeTenant?ListAll=True" + actions={actions} + offCanvas={offCanvas} + simpleColumns={columns} + filters={filters} + showTenantSelector={showTenantSelector} + showAllTenantsSelector={showAllTenantsSelector} + /> + {showCardButton && !onRefreshButtonClick && ( + + )} + + ); +}; + +export default CippTenantTable; diff --git a/src/components/CippWizard/CippWizard.jsx b/src/components/CippWizard/CippWizard.jsx index f0cc5f3013b3..b1a5457be67e 100644 --- a/src/components/CippWizard/CippWizard.jsx +++ b/src/components/CippWizard/CippWizard.jsx @@ -1,11 +1,29 @@ import { useCallback, useMemo, useState } from "react"; import { Card, CardContent, Container, Stack } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { WizardSteps } from "./wizard-steps"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; export const CippWizard = (props) => { const { postUrl, orientation = "horizontal", steps } = props; + + const formControl = useForm({ mode: "onChange", defaultValues: props.initialState }); + const formWatcher = useWatch({ + control: formControl.control, + }); + + const stepsWithVisibility = useMemo(() => { + return steps.filter((step) => { + if (step.hideStepWhen) { + return !step.hideStepWhen(formWatcher); + } + if (step.showStepWhen) { + return step.showStepWhen(formWatcher); + } + return true; + }); + }, [steps, formWatcher]); + const [activeStep, setActiveStep] = useState(0); const handleBack = useCallback(() => { setActiveStep((prevState) => (prevState > 0 ? prevState - 1 : prevState)); @@ -14,40 +32,40 @@ export const CippWizard = (props) => { const handleNext = useCallback(() => { setActiveStep((prevState) => (prevState < steps.length - 1 ? prevState + 1 : prevState)); }, []); - const formControl = useForm({ mode: "onChange", defaultValues: props.initialState }); + const content = useMemo(() => { - const StepComponent = steps[activeStep].component; + const StepComponent = stepsWithVisibility[activeStep].component; return ( ); - }, [activeStep, handleNext, handleBack, steps, formControl]); + }, [activeStep, handleNext, handleBack, stepsWithVisibility, formControl]); return ( {orientation === "vertical" ? ( - + - + {content} @@ -59,7 +77,7 @@ export const CippWizard = (props) => { postUrl={postUrl} activeStep={activeStep} orientation={orientation} - steps={steps} + steps={stepsWithVisibility} />
    {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) => { }} > + + Setup Wizard + + + + + {!setupCompleted && ( + + + + Setup has not been completed. + + + + + )} {(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? ( @@ -255,30 +288,7 @@ export const Layout = (props) => { ) : ( - <> - - Setup Wizard - - - - - {!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 - + - sponsor window.open(randomimg.link)} - width={"100px"} - /> - - - )} - - - + {renderItems({ + collapse, + depth: 0, + items: processedItems, + pathname, + })} + {" "} + {/* Add this closing tag */} + {profile?.clientPrincipal && ( + <> + + + This application is sponsored by + + + sponsor 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 */} - + 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()}) + + )} + + {/* Date Filter */} - + + + Use the filters below to narrow down your logbook results. You can filter by + date range, username, and severity levels. By default, the logbook shows the + current day based on UTC time. Your local time is {new Date().getTimezoneOffset() / -60} hours offset from UTC. + + + + + + + + + { + const startDate = formControl.getValues("startDate"); + if (value && !startDate) { + return "Start date must be set when using an end date"; + } + if ( + startDate && + value && + new Date(value * 1000) < new Date(startDate * 1000) + ) { + return "End date must be after start date"; + } + return true; + }, + }} + /> + + + + + {/* Date Range Warning Alert */} + + + {/* Username Filter */} + - {/* Toggle Switch */} - + {/* Severity Filter */} + - {/* Submit Button */} - - + {/* Action Buttons */} + + + + + @@ -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 = [ + <> + + + { + 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 }} + > + + {Object.entries(item) + .filter(([key]) => key !== "language") + .map(([key, value]) => ( + + + {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} + + + {value} + + + ))} + + + ), + })), + } + } + : {}; + + // Simplified columns for the table + const simpleColumns = [ + "Name", + "ReleaseActionPreference", + "Delete", + "Preview", + "BlockSender", + "AllowSender", + "QuarantineNotification", + "IncludeMessagesFromBlockedSenderAddress", + "WhenCreated", + "WhenChanged", + ]; + + + + // Prepare data for CippInfoBar as a const to clean up the code + const infoBarData = [ + { + icon: , + data: globalQuarantineData?.EndUserSpamNotificationFrequency, + name: "Notification Frequency", + }, + { + icon: , + data: hasGlobalQuarantinePolicyData + ? (globalQuarantineData?.OrganizationBrandingEnabled + ? "Enabled" + : "Disabled" + ) + : "n/a", + name: "Branding", + }, + { + icon: , + data: hasGlobalQuarantinePolicyData + ? (globalQuarantineData?.EndUserSpamNotificationCustomFromAddress + ? globalQuarantineData?.EndUserSpamNotificationCustomFromAddress + : "None") + : "n/a" , + name: "Custom Sender Address", + }, + { + icon: , + toolTip: "More Info", + data: hasGlobalQuarantinePolicyData + ? ( + multiLanguagePropertyItems.length > 0 + ? multiLanguagePropertyItems.map(item => item.language).join(", ") + : "None" + ) + : "n/a", + name: "Custom Language", + ...customLanguageOffcanvas, + }, + ]; + + return ( + <> + + + + + + + + + + + + + } + /> + + + ); +}; + +// Layout configuration: ensure page uses DashboardLayout +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/spamfilter/list-spamfilter/add.jsx b/src/pages/email/spamfilter/list-spamfilter/add.jsx index 893f45c7ee51..bf5cc6fb10c1 100644 --- a/src/pages/email/spamfilter/list-spamfilter/add.jsx +++ b/src/pages/email/spamfilter/list-spamfilter/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/AddSpamFilter" > - + { {/* TemplateList */} - + { - + { postUrl="/api/AddExConnector" > - + { {/* TemplateList */} - + { - + { postUrl="/api/AddTransportRule" > - + { {/* TemplateList */} - + { - + { actions: actions, }; - const simpleColumns = ["Name", "State", "Mode", "RuleErrorAction", "WhenChanged", "Comments"]; + const simpleColumns = [ + "Name", + "State", + "Mode", + "RuleErrorAction", + "WhenChanged", + "Comments", + "Tenant", + ]; return ( { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 17197aff1e9b..e3a66509ea79 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -15,6 +15,7 @@ import { Archive, AutoMode, Recycling, + ManageAccounts, } from "@mui/icons-material"; const Page = () => { @@ -31,6 +32,43 @@ const Page = () => { multiPost: false, external: true, }, + { + label: "Change Primary User", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "!users", + }, + fields: [ + { + type: "autoComplete", + name: "user", + label: "Select User", + multiple: false, + creatable: false, + api: { + url: "/api/ListGraphRequest", + data: { + Endpoint: "users", + $select: "id,displayName,userPrincipalName", + $top: 999, + $count: true, + }, + queryKey: "ListUsersAutoComplete", + dataKey: "Results", + labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, + valueField: "id", + addedField: { + userPrincipalName: "userPrincipalName", + }, + showRefresh: true, + }, + }, + ], + confirmText: "Select the User to set as the primary user for this device", + }, { label: "Sync Device", type: "POST", diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx index eb2851b0544f..ee7ed45f4f84 100644 --- a/src/pages/endpoint/MEM/list-scripts/index.jsx +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -18,7 +18,6 @@ import { Search, Close, Save } from "@mui/icons-material"; import { useSettings } from "../../../../hooks/use-settings"; import { Stack } from "@mui/system"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import axios from "axios"; const Page = () => { const pageTitle = "Scripts"; @@ -94,17 +93,19 @@ const Page = () => { scriptContent, runAsAccount, fileName, - roleScopeTagIds + roleScopeTagIds, + scriptType, } = currentScript; const patchData = { TenantFilter: tenantFilter, ScriptId: id, + ScriptType: scriptType, IntuneScript: JSON.stringify({ runAs32Bit, id, displayName, description, - scriptContent: scriptBytes.toString("base64"), // Convert to base64 + scriptContent: scriptBytes.toString("base64"), // Convert to base64 runAsAccount, fileName, roleScopeTagIds, @@ -118,7 +119,7 @@ const Page = () => { }); if (!response.ok) { - dispatch ( + dispatch( showToast({ title: "Script Save Error", message: "Your Intune script could not be saved.", @@ -126,7 +127,7 @@ const Page = () => { }) ); } - + return response.json(); }, enabled: false, @@ -139,7 +140,7 @@ const Page = () => { const { data } = await saveScriptRefetch(); setCodeContentChanged(false); setCodeOpen(!codeOpen); - dispatch ( + dispatch( showToast({ title: "Script Saved", message: "Your Intune script has been saved successfully.", @@ -228,15 +229,12 @@ const Page = () => { )} {isSaving && ( - + )} {(scriptIsFetching || scriptIsLoading) && } - {(!scriptIsFetching && !scriptIsLoading) && ( + {!scriptIsFetching && !scriptIsLoading && ( { }; Page.getLayout = (page) => {page}; -export default Page; \ No newline at end of file +export default Page; diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx index d82b40deeceb..8bad92e4b715 100644 --- a/src/pages/endpoint/applications/list/add.jsx +++ b/src/pages/endpoint/applications/list/add.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; -import { Grid, Divider, Button } from "@mui/material"; +import { Divider, Button } 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"; @@ -49,11 +50,11 @@ const ApplicationDeploymentForm = () => { }; const ChocosearchResults = ApiPostCall({ - urlfromData: true, + urlFromData: true, }); const winGetSearchResults = ApiPostCall({ - urlfromData: true, + urlFromData: true, }); const searchApp = (searchText, type) => { @@ -92,7 +93,7 @@ const ApplicationDeploymentForm = () => { }} > - + { {/* Tenant Selector */} - + { compareType="is" compareValue="mspApp" > - + { validators={{ required: "Please select an MSP Tool" }} /> - + { compareType="is" compareValue="datto" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { compareValue="syncro" > {selectedTenants?.map((tenant, index) => ( - + { compareType="is" compareValue="huntress" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { compareType="is" compareValue="automate" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { ))} {selectedTenants?.map((tenant, index) => ( - + { compareValue="cwcommand" > {selectedTenants?.map((tenant, index) => ( - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { compareType="is" compareValue="StoreApp" > - + { formControl={formControl} /> - + - + { isFetching={winGetSearchResults.isLoading} /> - + { validators={{ required: "Package Identifier is required" }} /> - + { validators={{ required: "Application Name is required" }} /> - + { {/* Install Options */} - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { compareType="is" compareValue="chocolateyApp" > - + { formControl={formControl} /> - + - + { /> - + { validators={{ required: "Package Name is required" }} /> - + { validators={{ required: "Application Name is required" }} /> - + { formControl={formControl} /> - + { {/* Install Options */} - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { > {/* Office App Fields */} - + { formControl={formControl} /> - + { validators={{ required: "Please select an update channel" }} /> - + { validators={{ required: "Please select at least one language" }} /> - + { formControl={formControl} /> - + { defaultValue={true} /> - + { defaultValue={true} /> - + { {/* Assign To Options */} - + { > {/* Tenant Selector */} - + { {/* Form Fields */} - + { /> - + { {/* Switches */} - + { > {/* Tenant Selector */} - + { {/* Form Fields */} - + { /> - + { /> - + { /> - + { {/* Switches */} - + { + const pageTitle = "Work from anywhere Report"; + const tenantFilter = useSettings().currentTenant; + + // Actions from the source file + const actions = [ + { + label: "View in Intune", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + ]; + + // OffCanvas details based on the source file + const offCanvas = { + extendedInfoFields: [ + "id", + "deviceName", + "serialNumber", + "model", + "manufacturer", + "ownership", + "upgradeEligibility", + ], + actions: actions, + }; + + // Columns to be displayed in the table + const simpleColumns = [ + "deviceName", + "serialNumber", + "model", + "manufacturer", + "ownership", + "managedBy", + "osVersion", + "upgradeEligibility", + "ramCheckFailed", + "storageCheckFailed", + "processorCoreCountCheckFailed", + "processorSpeedCheckFailed", + "tpmCheckFailed", + "secureBootCheckFailed", + "processorFamilyCheckFailed", + "processor64BitCheckFailed", + "osCheckFailed", + ]; + + // Predefined filters to be applied to the table + const filterList = [ + { + filterName: "Upgrade not eligible", + value: [{ id: "upgradeEligibility", value: "notCapable" }], + type: "column", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/fullPageLoading.js b/src/pages/fullPageLoading.js index 12b1d3f961f9..67412ff6bc8e 100644 --- a/src/pages/fullPageLoading.js +++ b/src/pages/fullPageLoading.js @@ -1,7 +1,7 @@ -import { Grid } from "@mui/material"; +import "@mui/material"; import { Layout as DashboardLayout } from "../layouts/index.js"; import Head from "next/head.js"; -import { Box, Container, Stack } from "@mui/system"; +import { Box, Container, Stack, Grid } from "@mui/system"; const FullPageLoading = () => { return ( @@ -17,7 +17,7 @@ const FullPageLoading = () => { - + Loading... #Todo: Make pretty, make it obey user settings for theme. diff --git a/src/pages/identity/administration/deleted-items/index.js b/src/pages/identity/administration/deleted-items/index.js index b82213e9e8c9..66d27c8bfd83 100644 --- a/src/pages/identity/administration/deleted-items/index.js +++ b/src/pages/identity/administration/deleted-items/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import RestoreFromTrashIcon from "@mui/icons-material/RestoreFromTrash"; +import { RestoreFromTrash, Warning } from "@mui/icons-material"; const Page = () => { const pageTitle = "Deleted Items"; @@ -9,10 +9,20 @@ const Page = () => { { label: "Restore Object", type: "POST", - icon: , + icon: , url: "/api/ExecRestoreDeleted", - data: { ID: "id" }, - confirmText: "Are you sure you want to restore this user?", + data: { ID: "id", userPrincipalName: "userPrincipalName", displayName: "displayName" }, + confirmText: "Are you sure you want to restore this object?", + multiPost: false, + }, + { + label: "Permanently Delete Object", + type: "POST", + icon: , + url: "/api/RemoveDeletedObject", + data: { ID: "id", userPrincipalName: "userPrincipalName", displayName: "displayName" }, + confirmText: + "Are you sure you want to permanently delete this object? This action cannot be undone.", multiPost: false, }, ]; diff --git a/src/pages/identity/administration/devices/index.js b/src/pages/identity/administration/devices/index.js index 09bd9ef0d42f..c547a0d800b1 100644 --- a/src/pages/identity/administration/devices/index.js +++ b/src/pages/identity/administration/devices/index.js @@ -27,6 +27,7 @@ const Page = () => { }, confirmText: "Are you sure you want to enable this device?", multiPost: false, + condition: (row) => !row.accountEnabled, icon: , }, { @@ -39,6 +40,7 @@ const Page = () => { }, confirmText: "Are you sure you want to disable this device?", multiPost: false, + condition: (row) => row.accountEnabled, icon: , }, { @@ -80,7 +82,7 @@ const Page = () => { simpleColumns={[ "displayName", "accountEnabled", - "recipientType", + "trustType", "enrollmentType", "manufacturer", "model", diff --git a/src/pages/identity/administration/group-templates/edit.jsx b/src/pages/identity/administration/group-templates/edit.jsx new file mode 100644 index 000000000000..6f7b74c11cdf --- /dev/null +++ b/src/pages/identity/administration/group-templates/edit.jsx @@ -0,0 +1,106 @@ +import { Box, CircularProgress } from "@mui/material"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../hooks/use-settings"; +import CippAddGroupTemplateForm from "../../../../components/CippFormPages/CippAddGroupTemplateForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useEffect } from "react"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { id } = router.query; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Fetch template data + const { data: template, isFetching } = ApiGetCall({ + url: `/api/ListGroupTemplates?id=${id}`, + queryKey: `GroupTemplate-${id}`, + waiting: !!id, + }); + + // Map groupType values to valid radio options + const mapGroupType = (type) => { + // Map of group types to the corresponding option value + const groupTypeMap = { + // Standard mappings + azurerole: "azurerole", + generic: "generic", + m365: "m365", + dynamic: "dynamic", + dynamicdistribution: "dynamicdistribution", + distribution: "distribution", + security: "security", + + // Additional mappings from possible backend values + Unified: "m365", + Security: "generic", + Distribution: "distribution", + "Mail-enabled security": "security", + "Mail Enabled Security": "security", + "Azure Role Group": "azurerole", + "Azure Active Directory Role Group": "azurerole", + "Security Group": "generic", + "Microsoft 365 Group": "m365", + "Microsoft 365 (Unified)": "m365", + "Dynamic Group": "dynamic", + DynamicMembership: "dynamic", + "Dynamic Distribution Group": "dynamicdistribution", + DynamicDistribution: "dynamicdistribution", + "Distribution List": "distribution", + }; + + // Return just the value for the radio group, not the label/value pair + return groupTypeMap[type] || "generic"; // Default to generic if no mapping exists + }; + + // Set form values when template data is loaded + useEffect(() => { + if (template) { + const templateData = template[0]; + + // Make sure we have the necessary data before proceeding + if (templateData) { + formControl.reset({ + ...templateData, + groupType: mapGroupType(templateData.groupType), + tenantFilter: userSettingsDefaults.currentTenant, + }); + } + } + }, [template, formControl, userSettingsDefaults.currentTenant]); + + return ( + <> + + {/* Add debugging output to check what values are set */} +
    {JSON.stringify(formControl.watch(), null, 2)}
    + + + + +
    + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/group-templates/index.js b/src/pages/identity/administration/group-templates/index.js index 19cdc80add8f..6fe4acd3230a 100644 --- a/src/pages/identity/administration/group-templates/index.js +++ b/src/pages/identity/administration/group-templates/index.js @@ -1,10 +1,13 @@ import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { AddBox, RocketLaunch, Delete, GitHub } from "@mui/icons-material"; +import { AddBox, RocketLaunch, Delete, GitHub, Edit } from "@mui/icons-material"; import Link from "next/link"; import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; import { ApiGetCall } from "/src/api/ApiCall"; +import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; const Page = () => { const pageTitle = "Group Templates"; @@ -15,6 +18,11 @@ const Page = () => { refetchOnReconnect: false, }); const actions = [ + { + label: "Edit Template", + icon: , + link: "/identity/administration/group-templates/edit?id=[GUID]", + }, { label: "Save to GitHub", type: "POST", @@ -73,13 +81,37 @@ const Page = () => { ]; const offCanvas = { - children: (row) => , + children: (data) => { + const keys = Object.keys(data).filter( + (key) => !key.includes("@odata") && !key.includes("@data") + ); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + return ( + + ); + }, }; return ( @@ -92,7 +124,7 @@ const Page = () => { } offCanvas={offCanvas} - simpleColumns={["Displayname", "Description", "groupType", "GUID"]} + simpleColumns={["displayName", "description", "groupType", "GUID"]} /> ); }; diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index 1414c6738275..870a213c88f9 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Grid, Divider, Typography } from "@mui/material"; +import { Divider, Typography } 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"; @@ -90,7 +91,7 @@ const EditGroup = () => { return ( - + { > Add - + { {/* AddOwners */} - + { removeOptions={groupInfo.data?.owners?.map((o) => o.userPrincipalName)} /> - + { /> - + Remove { {/* RemoveOwners */} - + { multiple={true} /> - + { {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( - + { )} {groupType === "Microsoft 365" && ( - + { - + { - const formControl = useForm({ Mode: "onChange" }); - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ label: role.Name, value: role.ObjectId }))} - formControl={formControl} - /> - - - - - - - - - - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Box, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; +import gdaproles from "/src/data/GDAPRoles.json"; +import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; +import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; +const Page = () => { + const formControl = useForm({ Mode: "onChange" }); + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + { + if (!option?.value) { + return "Domain is required"; + } + return true; + }, + }} + /> + + + + + + + + + + + + + + { + if (!value) { + return "Start date is required"; + } + return true; + }, + }} + /> + + + { + const startDate = formControl.getValues("startDate"); + if (!value) { + return "End date is required"; + } + if (new Date(value) < new Date(startDate)) { + return "End date must be after start date"; + } + return true; + }, + }} + /> + + + ({ label: role.Name, value: role.ObjectId }))} + formControl={formControl} + required={true} + validators={{ + validate: (options) => { + if (!options?.length) { + return "At least one role is required"; + } + return true; + }, + }} + /> + + + + + + { + if (!option?.value) { + return "Expiration action is required"; + } + return true; + }, + }} + /> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/jit-admin/index.js b/src/pages/identity/administration/jit-admin/index.js index e4ad1c00ce46..ae2faff8bf5c 100644 --- a/src/pages/identity/administration/jit-admin/index.js +++ b/src/pages/identity/administration/jit-admin/index.js @@ -22,6 +22,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/identity/administration/users/invite.jsx b/src/pages/identity/administration/users/invite.jsx index cd755cfd9909..b5aa6014181c 100644 --- a/src/pages/identity/administration/users/invite.jsx +++ b/src/pages/identity/administration/users/invite.jsx @@ -1,4 +1,5 @@ -import { Box, Grid } from "@mui/material"; +import { Box } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; @@ -19,12 +20,12 @@ const Page = () => { - + diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index 9e8c1a59eb9e..632638a7987e 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -11,7 +11,7 @@ import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import CippRemediationCard from "../../../../../components/CippCards/CippRemediationCard"; import CippButtonCard from "../../../../../components/CippCards/CippButtonCard"; import { SvgIcon, Typography, CircularProgress, Button } from "@mui/material"; diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index d11ba05bc1ba..64d2f3f82b0e 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -1,263 +1,263 @@ -import { useState } from "react"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useSettings } from "/src/hooks/use-settings"; -import { useRouter } from "next/router"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; -import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Mail, Forward, Fingerprint, Launch } from "@mui/icons-material"; -import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; -import tabOptions from "./tabOptions"; -import ReactTimeAgo from "react-time-ago"; -import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; -import { Box, Stack, Typography, Button, CircularProgress } from "@mui/material"; -import Grid from "@mui/material/Grid"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import countryList from "/src/data/countryList"; -import { CippDataTable } from "/src/components/CippTable/CippDataTable"; -import { useForm } from "react-hook-form"; -import CippButtonCard from "../../../../../components/CippCards/CippButtonCard"; -import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; -import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { userId } = router.query; - - const tenant = userSettingsDefaults.currentTenant; - const [formParams, setFormParams] = useState(false); - - const userRequest = ApiGetCall({ - url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`, - queryKey: `ListUsers-${userId}`, - }); - - // Set the title and subtitle for the layout - const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; - - const subtitle = userRequest.isSuccess - ? [ - { - icon: , - text: , - }, - { - icon: , - text: , - }, - { - icon: , - text: ( - <> - Created: - - ), - }, - { - icon: , - text: ( - - ), - }, - ] - : []; - - // Initialize React Hook Form - const formControl = useForm(); - - const postRequest = ApiPostCall({ - url: "/api/ExecCACheck", - relatedQueryKeys: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, - }); - const onSubmit = (data) => { - //add userId and tenantFilter to the object - data.userId = {}; - data.userId["value"] = userId; - data.tenantFilter = tenant; - setFormParams(data); - postRequest.mutate({ - url: "/api/ExecCACheck", - data: data, - queryKey: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, - }); - }; - - return ( - - {userRequest.isLoading && } - {userRequest.isSuccess && ( - - - {/* Form Section */} - - - Test policies - - } - > - {/* Form Starts Here */} -
    - - Test your conditional access policies before putting them in production. The - returned results will show you if the user is allowed or denied access based on - the policy. - - - - {/* Mandatory Parameters */} - Mandatory Parameters: - `${option.displayName}`, - valueField: "id", - queryKey: `ServicePrincipals-${tenant}`, - data: { - Endpoint: "ServicePrincipals", - manualPagination: true, - $select: "id,displayName", - $count: true, - $orderby: "displayName", - $top: 999, - }, - }} - formControl={formControl} - /> - - {/* Optional Parameters */} - Optional Parameters: - - {/* Test from this country */} - ({ - value: Code, - label: Name, - }))} - formControl={formControl} - /> - - {/* Test from this IP */} - - - {/* Device Platform */} - - - {/* Client Application Type */} - - - {/* Sign-in risk level */} - - - {/* User risk level */} - - - -
    -
    -
    - - - -
    -
    - )} -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { useState } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; +import { Mail, Forward, Fingerprint, Launch } from "@mui/icons-material"; +import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; +import tabOptions from "./tabOptions"; +import ReactTimeAgo from "react-time-ago"; +import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; +import { Box, Stack, Typography, Button, CircularProgress } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import countryList from "/src/data/countryList"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { useForm } from "react-hook-form"; +import CippButtonCard from "../../../../../components/CippCards/CippButtonCard"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { userId } = router.query; + + const tenant = userSettingsDefaults.currentTenant; + const [formParams, setFormParams] = useState(false); + + const userRequest = ApiGetCall({ + url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`, + queryKey: `ListUsers-${userId}`, + }); + + // Set the title and subtitle for the layout + const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; + + const subtitle = userRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Created: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + + // Initialize React Hook Form + const formControl = useForm(); + + const postRequest = ApiPostCall({ + url: "/api/ExecCACheck", + relatedQueryKeys: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, + }); + const onSubmit = (data) => { + //add userId and tenantFilter to the object + data.userId = {}; + data.userId["value"] = userId; + data.tenantFilter = tenant; + setFormParams(data); + postRequest.mutate({ + url: "/api/ExecCACheck", + data: data, + queryKey: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`, + }); + }; + + return ( + + {userRequest.isLoading && } + {userRequest.isSuccess && ( + + + {/* Form Section */} + + + Test policies + + } + > + {/* Form Starts Here */} +
    + + Test your conditional access policies before putting them in production. The + returned results will show you if the user is allowed or denied access based on + the policy. + + + + {/* Mandatory Parameters */} + Mandatory Parameters: + `${option.displayName}`, + valueField: "id", + queryKey: `ServicePrincipals-${tenant}`, + data: { + Endpoint: "ServicePrincipals", + manualPagination: true, + $select: "id,displayName", + $count: true, + $orderby: "displayName", + $top: 999, + }, + }} + formControl={formControl} + /> + + {/* Optional Parameters */} + Optional Parameters: + + {/* Test from this country */} + ({ + value: Code, + label: Name, + }))} + formControl={formControl} + /> + + {/* Test from this IP */} + + + {/* Device Platform */} + + + {/* Client Application Type */} + + + {/* Sign-in risk level */} + + + {/* User risk level */} + + + +
    +
    +
    + + + +
    +
    + )} +
    + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/user/devices.jsx b/src/pages/identity/administration/users/user/devices.jsx index 23cda503df3a..41a1e73edd2d 100644 --- a/src/pages/identity/administration/users/user/devices.jsx +++ b/src/pages/identity/administration/users/user/devices.jsx @@ -10,7 +10,7 @@ import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard"; import { Typography } from "@mui/material"; import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 9d3c263f3b95..2ce3578fa876 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -10,7 +10,7 @@ import tabOptions from "./tabOptions"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; import { CippExchangeInfoCard } from "../../../../../components/CippCards/CippExchangeInfoCard"; import { useEffect, useState } from "react"; diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index e91db17cadfc..1bc1412818bf 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -9,7 +9,7 @@ import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayou import tabOptions from "./tabOptions"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard"; import { SvgIcon, Typography } from "@mui/material"; import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; @@ -87,7 +87,7 @@ const Page = () => { }); const userBulkRequest = ApiPostCall({ - urlfromdata: true, + urlFromData: true, }); useEffect(() => { diff --git a/src/pages/identity/reports/signin-report/index.js b/src/pages/identity/reports/signin-report/index.js index 619f99ef8bcd..9928b10dae07 100644 --- a/src/pages/identity/reports/signin-report/index.js +++ b/src/pages/identity/reports/signin-report/index.js @@ -1,7 +1,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { useState } from "react"; -import { Button, Grid, TextField, Switch, FormControlLabel } from "@mui/material"; +import { Button, TextField, Switch, FormControlLabel } from "@mui/material"; +import { Grid } from "@mui/system"; import CippButtonCard from "/src/components/CippCards/CippButtonCard"; const Page = () => { @@ -42,7 +43,7 @@ const Page = () => { const tableFilter = ( - + { fullWidth /> - + { fullWidth /> - + { /> {filterValues.failedLogonsOnly && ( - + { /> )} - + diff --git a/src/pages/index.js b/src/pages/index.js index 91041ac208ea..d12ed2729f1b 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,6 +1,7 @@ import Head from "next/head"; import { useEffect, useState } from "react"; -import { Box, Container, Grid, Button } from "@mui/material"; +import { Box, Container, Button } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippInfoBar } from "../components/CippCards/CippInfoBar"; import { CippChartCard } from "../components/CippCards/CippChartCard"; import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; @@ -186,20 +187,20 @@ const Page = () => { - + - + - + - + { /> - + { /> - + { {/* Converted Domain Names to Property List */} - + { /> - + { /> - + { + const [loadingText, setLoadingText] = useState("Please wait while we log you in..."); + const orgData = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + }); + + const [loadingImage, setLoadingImage] = useState( + "/assets/illustrations/undraw_analysis_dq08.svg" + ); + + useEffect(() => { + const timer = setTimeout(() => { + if (!orgData.isSuccess) { + setLoadingText( + "The function app may be experiencing a cold start currently, this can take a little longer than usual..." + ); + setLoadingImage("/assets/illustrations/undraw-into-the-night-nd84.svg"); + } + }, 20000); // 20 seconds + + return () => clearTimeout(timer); + }, [orgData.isSuccess]); + + return ( + <> + + + Loading + + + + + + + + + + + + + + + ); +}; + +export default Page; diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js new file mode 100644 index 000000000000..ee7f73445035 --- /dev/null +++ b/src/pages/onboardingv2.js @@ -0,0 +1,120 @@ +import { Layout as DashboardLayout } from "../layouts/index.js"; +import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.jsx"; +import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.jsx"; +import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx"; +import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx"; +import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx"; +import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx"; +import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx"; +import { CippNotificationsStep } from "../components/CippWizard/CippNotificationsStep.jsx"; +import { CippAlertsStep } from "../components/CippWizard/CippAlertsStep.jsx"; +import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const steps = [ + { + description: "Onboarding", + component: CippWizardOptionsList, + componentProps: { + title: "Select your setup method", + subtext: `This wizard will guide you through setting up CIPPs access to your client tenants. If this is your first time setting up CIPP you will want to choose the option "Create application for me and connect to my tenants",`, + valuesKey: "SyncTool", + options: [ + { + description: + "Choose this option if this is your first setup, or if you'd like to redo the previous setup.", + icon: , + label: "First Setup", + value: "FirstSetup", + }, + { + description: + "Choose this option if you would like to add a tenant to your environment.", + icon: , + label: "Add a tenant", + value: "AddTenant", + }, + { + description: + "Choose this option if you want to setup which application registration is used to connect to your tenants.", + icon: , + label: "Create a new application registration for me and connect to my tenants", + value: "CreateApp", + }, + { + description: "I would like to refresh my token or replace the account I've used.", + icon: , + label: "Refresh Tokens for existing application registration", + value: "UpdateTokens", + }, + { + description: + "I have an existing application and would like to manually enter my token, or update them. This is only recommended for advanced users.", + icon: , + label: "Manually enter credentials", + value: "Manual", + }, + ], + }, + }, + { + description: "Application", + component: CippSAMDeploy, + showStepWhen: (values) => + values?.selectedOption === "CreateApp" || values?.selectedOption === "FirstSetup", + }, + { + description: "Tenants", + component: CippTenantModeDeploy, + showStepWhen: (values) => + values?.selectedOption === "CreateApp" || + values?.selectedOption === "FirstSetup" || + values?.selectedOption === "AddTenant", + }, + { + description: "Baselines", + component: CippBaselinesStep, + showStepWhen: (values) => values?.selectedOption === "FirstSetup", + }, + { + description: "Notifications", + component: CippNotificationsStep, + showStepWhen: (values) => values?.selectedOption === "FirstSetup", + }, + { + description: "Next Steps", + component: CippAlertsStep, + showStepWhen: (values) => values?.selectedOption === "FirstSetup", + }, + { + description: "Refresh Tokens", + component: CippDeploymentStep, + showStepWhen: (values) => values?.selectedOption === "UpdateTokens", + }, + { + description: "Manually enter credentials", + component: CippDeploymentStep, + showStepWhen: (values) => values?.selectedOption === "Manual", + }, + { + description: "Confirmation", + component: CippWizardConfirmation, + //confirm and finish button, perform tasks, launch checks etc. + }, + ]; + + return ( + <> + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 8a3007ec7a99..14b6be90c7fd 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -1,5 +1,6 @@ import React from "react"; -import { Grid, Typography, Divider } from "@mui/material"; +import { Typography, 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"; @@ -25,7 +26,7 @@ const DeployDefenderForm = () => { - + { {/* Defender Setup Section */} - + { compareType="is" compareValue={true} > - + Defender Setup Defender and MEM Reporting - + - + { formControl={formControl} /> - + { {/* Defender Defaults Policy Section */} - + { compareType="is" compareValue={true} > - + Defender Defaults Policy Select Defender policies to deploy - + - + { formControl={formControl} /> - + { {/* Assign to Group */} - + Assign to Group { { label: "Assign to all users and devices", value: "AllDevicesAndUsers" }, ]} formControl={formControl} + validators={{ required: "Assignment must be selected" }} row /> @@ -297,7 +299,7 @@ const DeployDefenderForm = () => { {/* ASR Section */} - + { compareType="is" compareValue={true} > - + ASR Rules Set Attack Surface Reduction Rules + - + - + + { name="ASR.WMIPersistence" formControl={formControl} /> + { name="ASR.BlockOfficeApps" formControl={formControl} /> + - + { name="ASR.blockJSVB" formControl={formControl} /> + { {/* Assign to Group */} - + Assign to Group { { label: "Assign to all users and devices", value: "AllDevicesAndUsers" }, ]} formControl={formControl} + validators={{ required: "Assignment must be selected" }} row /> diff --git a/src/pages/security/incidents/list-incidents/index.js b/src/pages/security/incidents/list-incidents/index.js index 8db8ce596132..354a86355a8f 100644 --- a/src/pages/security/incidents/list-incidents/index.js +++ b/src/pages/security/incidents/list-incidents/index.js @@ -99,6 +99,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/teams-share/sharepoint/add-site.js b/src/pages/teams-share/sharepoint/add-site.js index cf3dbd103463..23b7e08c64ef 100644 --- a/src/pages/teams-share/sharepoint/add-site.js +++ b/src/pages/teams-share/sharepoint/add-site.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { Grid } from "@mui/material"; +import "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -22,10 +23,10 @@ const AddSiteForm = () => { backButtonTitle="Back to Sites" > - + - + { required /> - + { }} /> - + { }} /> - + { > {/* Display Name */} - + { {/* Description */} - + { - + { {/* Visibility */} - + { const apiRequest = ApiPostCall({ - relatedQueryKeys: "ListAlertsQueue", + relatedQueryKeys: ["ListAlertsQueue", "ListCurrentAlerts"], }); const router = useRouter(); const [editAlert, setAlertEdit] = useState(false); @@ -45,6 +46,7 @@ const AlertWizard = () => { const existingAlert = ApiGetCall({ url: "/api/ListAlertsQueue", relatedQueryKeys: "ListAlertsQueue", + queryKey: "ListCurrentAlerts", }); const [recurrenceOptions, setRecurrenceOptions] = useState([ { value: "30m", label: "Every 30 minutes" }, @@ -104,20 +106,38 @@ const AlertWizard = () => { alert.RawAlert.PostExecution.split(",").includes(opt.value) ); - // Reset the form with all values at once - formControl.reset( - { - tenantFilter: { - value: alert.RawAlert.Tenant, - label: alert.RawAlert.Tenant, - }, - excludedTenants: excludedTenantsFormatted, - command: { value: usedCommand, label: usedCommand.label }, - recurrence: recurrenceOption, - postExecution: postExecutionValue, + // Create the reset object with all the form values + const resetObject = { + tenantFilter: { + value: alert.RawAlert.Tenant, + label: alert.RawAlert.Tenant, }, - { keepDirty: false } - ); + excludedTenants: excludedTenantsFormatted, + command: { value: usedCommand, label: usedCommand.label }, + recurrence: recurrenceOption, + postExecution: postExecutionValue, + }; + + // Parse Parameters field if it exists and is a string + if (usedCommand?.requiresInput && alert.RawAlert.Parameters) { + try { + // Check if Parameters is a string that needs parsing + const params = + typeof alert.RawAlert.Parameters === "string" + ? JSON.parse(alert.RawAlert.Parameters) + : alert.RawAlert.Parameters; + + // Set the input value if it exists + if (params.InputValue) { + resetObject[usedCommand.inputName] = params.InputValue; + } + } catch (error) { + console.error("Error parsing parameters:", error); + } + } + + // Reset the form with all values at once + formControl.reset(resetObject, { keepDirty: false }); } if (alert?.PartitionKey === "Webhookv2") { setAlertType("audit"); @@ -134,7 +154,7 @@ const AlertWizard = () => { formControl.reset({ RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined, tenantFilter: alert.RawAlert.Tenants, - excludedTenants: alert.RawAlert.excludedTenants, + excludedTenants: alert.excludedTenants, Actions: alert.RawAlert.Actions, conditions: alert.RawAlert.Conditions, logbook: foundLogbook, @@ -288,7 +308,7 @@ const AlertWizard = () => { - + setAlertType("audit")}> @@ -300,7 +320,7 @@ const AlertWizard = () => { - + setAlertType("script")}> @@ -315,19 +335,25 @@ const AlertWizard = () => { {/* Audit Log Form */} {alertType === "audit" && ( - - + +
    - - + + - + { compareType="valueContains" compareValue="AllTenants" > - + { - + { sx={{ mb: 3 }} > - + { /> - + { /> + + + {addedEvent.map((event) => ( - + { options={getAuditLogSchema(logbookWatcher?.value)} /> - + { ]} /> - + { field={`conditions.${event.id}.Property`} formControl={formControl} compareType="contains" - compareValue={"List:"} + compareValue="List:" > { /> - - handleAddCondition()}> - - - - - handleRemoveCondition(event.id)} - > - - + + + handleRemoveCondition(event.id)} + > + + + ))} - + { required: { value: true, message: "This field is required" }, }} formControl={formControl} - multiple + multiple={true} + creatable={false} options={actionstoTake} /> - + @@ -501,18 +539,18 @@ const AlertWizard = () => { {/* Scripted CIPP Alert Form */} {alertType === "script" && ( - - + + - + - + { compareType="is" compareValue="AllTenants" > - + { - + { } > - + { }))} /> - + { options={recurrenceOptions} // Use the state-managed recurrenceOptions here /> - + {commandValue?.value?.requiresInput && ( { /> )} - + { required: { value: true, message: "This field is required" }, }} formControl={formControl} - multiple + multiple={true} + creatable={false} options={postExecutionOptions} /> - + diff --git a/src/pages/tenant/administration/app-consent-requests/index.js b/src/pages/tenant/administration/app-consent-requests/index.js index 3bc6c53b58d6..6287465a1076 100644 --- a/src/pages/tenant/administration/app-consent-requests/index.js +++ b/src/pages/tenant/administration/app-consent-requests/index.js @@ -1,14 +1,8 @@ import { useState, useEffect, use } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { - Grid, - Button, - Accordion, - AccordionSummary, - AccordionDetails, - Typography, -} from "@mui/material"; +import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -62,7 +56,7 @@ const Page = () => { {/* Request Status Filter */} - + { {/* Submit Button */} - + @@ -100,7 +94,7 @@ const Page = () => { type: "column", }, ]} - queryKey={`AppConsentRequests-${JSON.stringify(filterParams)}`} + queryKey={`AppConsentRequests-${JSON.stringify(filterParams)}-${tenantFilter}`} apiData={{ ...filterParams, }} diff --git a/src/pages/tenant/administration/applications/app-registrations.js b/src/pages/tenant/administration/applications/app-registrations.js new file mode 100644 index 000000000000..ff9a643f1e92 --- /dev/null +++ b/src/pages/tenant/administration/applications/app-registrations.js @@ -0,0 +1,88 @@ +// this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83 +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Launch } from "@mui/icons-material"; +import tabOptions from "./tabOptions"; + +const Page = () => { + const pageTitle = "App Registrations"; + const apiUrl = "/api/ListGraphRequest"; + + const actions = [ + { + icon: , + label: "View App Registration", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + }, + { + icon: , + label: "View API Permissions", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "displayName", + "id", + "appId", + "createdDateTime", + "signInAudience", + "replyUrls", + "requiredResourceAccess", + "web", + "api", + "passwordCredentials", + "keyCredentials", + ], + actions: actions, + }; + + const simpleColumns = [ + "displayName", + "appId", + "createdDateTime", + "signInAudience", + "web.redirectUris", + "publisherDomain", + "passwordCredentials", + "keyCredentials", + ]; + + const apiParams = { + Endpoint: "applications", + $select: + "id,appId,displayName,createdDateTime,signInAudience,web,api,requiredResourceAccess,publisherDomain,replyUrls,passwordCredentials,keyCredentials", + $count: true, + $top: 999, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/enterprise-apps/index.js b/src/pages/tenant/administration/applications/enterprise-apps.js similarity index 77% rename from src/pages/tenant/administration/enterprise-apps/index.js rename to src/pages/tenant/administration/applications/enterprise-apps.js index 258e09ce23d8..b7c80840c4e4 100644 --- a/src/pages/tenant/administration/enterprise-apps/index.js +++ b/src/pages/tenant/administration/applications/enterprise-apps.js @@ -1,7 +1,9 @@ // this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83 import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Launch } from "@mui/icons-material"; +import tabOptions from "./tabOptions"; const Page = () => { const pageTitle = "Enterprise Applications"; @@ -17,16 +19,6 @@ const Page = () => { multiPost: false, external: true, }, - { - icon: , - label: "View App Registration", - link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/[appId]`, - color: "info", - target: "_blank", - multiPost: false, - external: true, - condition: (row) => row.tags.includes("WindowsAzureActiveDirectoryIntegratedApp"), - }, ]; const offCanvas = { @@ -48,12 +40,14 @@ const Page = () => { "createdDateTime", "publisherName", "homepage", + "passwordCredentials", + "keyCredentials", ]; const apiParams = { Endpoint: "servicePrincipals", $select: - "id,appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags", + "id,appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags,passwordCredentials,keyCredentials", $count: true, $top: 999, }; @@ -71,6 +65,10 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/administration/applications/permission-sets/add.js b/src/pages/tenant/administration/applications/permission-sets/add.js new file mode 100644 index 000000000000..112272d3a51e --- /dev/null +++ b/src/pages/tenant/administration/applications/permission-sets/add.js @@ -0,0 +1,227 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { Alert, CardContent, Skeleton, Stack, Typography, Button, Box } from "@mui/material"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; +import { useEffect, useState } from "react"; +import { CopyAll } from "@mui/icons-material"; + +const Page = () => { + const router = useRouter(); + const { template, copy, name } = router.query; + const pageTitle = copy ? "Copy Permission Set" : "Add Permission Set"; + + const [initialPermissions, setInitialPermissions] = useState(null); + const [availableTemplates, setAvailableTemplates] = useState([]); + // Add refetch key for refreshing data after save + const [refetchKey, setRefetchKey] = useState(0); + + const formControl = useForm({ + mode: "onBlur", + }); + + // Get the specified template if template ID is provided + const { data: templateData, isFetching: templateFetching } = ApiGetCall({ + url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null, + queryKey: template ? ["execAppPermissionTemplateDetails", template, refetchKey] : null, + enabled: !!template, + }); + + // Get all available templates for importing + const { data: allTemplates, isLoading: templatesLoading } = ApiGetCall({ + url: "/api/ExecAppPermissionTemplate", + queryKey: "execAppPermissionTemplateList", + }); + + useEffect(() => { + if (allTemplates && allTemplates.length > 0) { + setAvailableTemplates(allTemplates); + } + }, [allTemplates]); + + useEffect(() => { + // Initialize with empty structure for new templates + if (!template && !copy && !initialPermissions) { + setInitialPermissions({ + Permissions: {}, + TemplateName: "New Permission Set", + }); + formControl.setValue("templateName", "New Permission Set"); + } else if (templateData && copy && !initialPermissions) { + // When copying, we want to load the permissions but assign a new ID + if (templateData[0]) { + const copyName = `Copy of ${templateData[0].TemplateName}`; + setInitialPermissions({ + Permissions: templateData[0].Permissions, + TemplateName: copyName, + }); + formControl.setValue("templateName", copyName); + } + } else if (templateData && !initialPermissions) { + // For editing, keep the same ID + if (templateData[0]) { + setInitialPermissions({ + TemplateId: templateData[0].TemplateId, + Permissions: templateData[0].Permissions, + TemplateName: templateData[0].TemplateName, + }); + formControl.setValue("templateName", templateData[0].TemplateName); + } + } + }, [templateData, copy, template]); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ExecAppPermissionTemplate", "execAppPermissionTemplate"], + }); + + const handleUpdatePermissions = (data) => { + let payload = { + ...data, + }; + + if (copy) { + // For copy, ensure we're not sending the original ID + delete payload.TemplateId; + } else if (template && !copy) { + // For editing, include the template ID + payload.TemplateId = template; + } + + // Use the current value from the text field + payload.TemplateName = formControl.getValues("templateName"); + + updatePermissions.mutate( + { + url: "/api/ExecAppPermissionTemplate?Action=Save", + data: payload, + queryKey: "execAppPermissionTemplate", + }, + { + onSuccess: (data) => { + // Instead of navigating away, stay on the page and refresh + if (copy || !template) { + // If we're copying or creating new, update the URL to edit mode with the new template ID + const newTemplateId = data.data[0].Metadata.TemplateId; + router.push( + { + pathname: "/tenant/administration/applications/permission-sets/edit", + query: { + template: newTemplateId, + name: payload.TemplateName, + }, + }, + undefined, + { shallow: true } + ); + } else { + // Otherwise just refresh the current data + setRefetchKey((prev) => prev + 1); + } + }, + } + ); + }; + + const handleImportTemplate = () => { + const importTemplate = formControl.getValues("importTemplate"); + if (!importTemplate) return; + + const selectedTemplate = availableTemplates.find((t) => t.TemplateId === importTemplate.value); + if (selectedTemplate) { + setInitialPermissions({ + Permissions: selectedTemplate.Permissions, + TemplateName: `Import of ${selectedTemplate.TemplateName}`, + }); + formControl.setValue("templateName", `Import of ${selectedTemplate.TemplateName}`); + } + }; + + return ( + + + + {(!templateFetching || !template) && ( + <> + + {copy + ? "Create a copy of an existing permission set with your own modifications." + : template + ? "Edit the permissions in this permission set." + : "Create a new permission set to define a collection of application permissions."} + + + Permission sets allow you to define collections of permissions that can be applied + to applications consistently. + + + + + {!template && !copy && ( + + + ({ + label: template.TemplateName, + value: template.TemplateId, + }))} + isFetching={templatesLoading} + multiple={false} + /> + + + + )} + + {initialPermissions && ( + <> + + Choose the permissions you want to assign to this permission set. Microsoft + Graph is the default Service Principal added and you can choose to add + additional Service Principals as needed. Note that some Service Principals do + not have any published permissions to choose from. + + + + )} + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/permission-sets/edit.js b/src/pages/tenant/administration/applications/permission-sets/edit.js new file mode 100644 index 000000000000..e14272fb7b76 --- /dev/null +++ b/src/pages/tenant/administration/applications/permission-sets/edit.js @@ -0,0 +1,163 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { Alert, CardContent, Skeleton, Stack, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { Button } from "@mui/material"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; + +const Page = () => { + const router = useRouter(); + const { template, name } = router.query; + + const [initialPermissions, setInitialPermissions] = useState(null); + + const formControl = useForm({ + mode: "onBlur", + }); + + // Add a key to force refetch when we save + const [refetchKey, setRefetchKey] = useState(0); + + const { + data: templateData, + isFetching, + refetch, + } = ApiGetCall({ + url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null, + queryKey: template ? ["execAppPermissionTemplate", template, refetchKey] : null, + enabled: !!template, + }); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ExecAppPermissionTemplate", "execAppPermissionTemplate"], + }); + + useEffect(() => { + if (templateData && templateData[0]) { + setInitialPermissions({ + TemplateId: templateData[0].TemplateId, + Permissions: templateData[0].Permissions, + TemplateName: templateData[0].TemplateName, + }); + formControl.setValue("templateName", templateData[0].TemplateName, { + shouldValidate: true, + shouldDirty: false, + }); + } + }, [templateData]); + + const handleUpdatePermissions = (data) => { + let payload = { + ...data, + TemplateId: template, + }; + + // Use the current value from the text field + payload.TemplateName = formControl.getValues("templateName"); + + updatePermissions.mutate( + { + url: "/api/ExecAppPermissionTemplate?Action=Save", + data: payload, + queryKey: "execAppPermissionTemplate", + }, + { + onSuccess: () => { + // Refresh the data by incrementing the refetch key + setRefetchKey((prev) => prev + 1); + + // Explicitly refetch the data + refetch(); + }, + } + ); + }; + + // Instead of redirecting, we'll display an error message + if (!template) { + return ( + + + + The requested permission set does not exist or was not specified. Please select a valid + permission set from the list. + + + + + ); + } + + return ( + + + + {isFetching && ( + + )} + {!isFetching && initialPermissions && ( + <> + + Modify the permissions in this permission set. Any changes will affect all + applications using this permission set. + + + Permission sets allow you to define collections of permissions that can be applied + to applications consistently. + + + + + + Choose the permissions you want to assign to this permission set. Microsoft Graph is + the default Service Principal added and you can choose to add additional Service + Principals as needed. Note that some Service Principals do not have any published + permissions to choose from. + + + + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/permission-sets/index.js b/src/pages/tenant/administration/applications/permission-sets/index.js new file mode 100644 index 000000000000..41b02f844fc4 --- /dev/null +++ b/src/pages/tenant/administration/applications/permission-sets/index.js @@ -0,0 +1,75 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Edit, Delete, ContentCopy, Add } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import { Button } from "@mui/material"; +import Link from "next/link"; + +const Page = () => { + const pageTitle = "Permission Sets"; + const apiUrl = "/api/ExecAppPermissionTemplate"; + + const actions = [ + { + icon: , + label: "Edit Permission Set", + color: "warning", + link: "/tenant/administration/applications/permission-sets/edit?template=[TemplateId]&name=[TemplateName]", + }, + { + icon: , + label: "Copy Permission Set", + color: "info", + link: "/tenant/administration/applications/permission-sets/add?template=[TemplateId]©=true&name=[TemplateName]", + }, + { + icon: , + label: "Delete Permission Set", + color: "danger", + url: apiUrl, + data: { + Action: "Delete", + TemplateId: "TemplateId", + }, + type: "POST", + confirmText: "Are you sure you want to delete [TemplateName]?", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["TemplateName", "Permissions", "UpdatedBy", "Timestamp"], + actions: actions, + }; + + const simpleColumns = ["TemplateName", "Permissions", "UpdatedBy", "Timestamp"]; + + return ( + } + > + Add Permission Set + + } + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/applications/tabOptions.json b/src/pages/tenant/administration/applications/tabOptions.json new file mode 100644 index 000000000000..804316b7d5b8 --- /dev/null +++ b/src/pages/tenant/administration/applications/tabOptions.json @@ -0,0 +1,18 @@ +[ + { + "label": "Enterprise Apps", + "path": "/tenant/administration/applications/enterprise-apps" + }, + { + "label": "App Registrations", + "path": "/tenant/administration/applications/app-registrations" + }, + { + "label": "Permission Sets", + "path": "/tenant/administration/applications/permission-sets" + }, + { + "label": "Templates", + "path": "/tenant/administration/applications/templates" + } +] \ No newline at end of file diff --git a/src/pages/tenant/administration/applications/templates/add.js b/src/pages/tenant/administration/applications/templates/add.js new file mode 100644 index 000000000000..1fdbda3ca8c8 --- /dev/null +++ b/src/pages/tenant/administration/applications/templates/add.js @@ -0,0 +1,96 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CardContent } from "@mui/material"; +import { useEffect, useState } from "react"; +import AppApprovalTemplateForm from "/src/components/CippComponents/AppApprovalTemplateForm"; + +const Page = () => { + const router = useRouter(); + const { template, copy, name } = router.query; + const pageTitle = copy ? "Copy App Approval Template" : "Add App Approval Template"; + + // Add refetch key for refreshing data after save + const [refetchKey, setRefetchKey] = useState(0); + + const formControl = useForm({ + mode: "onBlur", + }); + + // Get the specified template if template ID is provided + const { data: templateData, isLoading: templateLoading } = ApiGetCall({ + url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null, + queryKey: template ? ["ExecAppApprovalTemplate", template, refetchKey] : null, + waiting: !!template, + }); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListAppApprovalTemplates", "ExecAppApprovalTemplate"], + }); + + const handleSubmit = (payload) => { + updatePermissions.mutate( + { + url: "/api/ExecAppApprovalTemplate?Action=Save", + data: payload, + queryKey: "ExecAppApprovalTemplate", + }, + { + onSuccess: (data) => { + // If we're adding or copying, redirect to edit page with the new ID + if (!template || copy) { + // Check if data exists and has the expected structure + const newTemplateId = data?.[0]?.TemplateId; + + if (newTemplateId) { + router.push( + { + pathname: "/tenant/administration/applications/templates/edit", + query: { + template: newTemplateId, + name: payload.TemplateName, + }, + }, + undefined, + { shallow: true } + ); + } else { + // Handle the case where TemplateId is missing + console.error("Missing TemplateId in response:", data); + // Just refresh the data as fallback + setRefetchKey((prev) => prev + 1); + } + } else { + // Just refresh the data if we're editing + setRefetchKey((prev) => prev + 1); + } + }, + } + ); + }; + + return ( + + + + + + ); +}; + +// Changed from TabbedLayout to just DashboardLayout +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/templates/edit.js b/src/pages/tenant/administration/applications/templates/edit.js new file mode 100644 index 000000000000..807717c763cf --- /dev/null +++ b/src/pages/tenant/administration/applications/templates/edit.js @@ -0,0 +1,104 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { Alert, Button, CardContent } from "@mui/material"; +import { useState } from "react"; +import Link from "next/link"; +import AppApprovalTemplateForm from "/src/components/CippComponents/AppApprovalTemplateForm"; + +const Page = () => { + const router = useRouter(); + const { template, name } = router.query; + + // Add a key to force refetch when we save + const [refetchKey, setRefetchKey] = useState(0); + + const formControl = useForm({ + mode: "onBlur", + }); + + const { data: templateData, isLoading } = ApiGetCall({ + url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null, + queryKey: template ? ["ExecAppApprovalTemplateList", template, refetchKey] : null, + enabled: !!template, + }); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListAppApprovalTemplates", "ExecAppApprovalTemplate"], + }); + + const handleSubmit = (payload) => { + // Ensure we're passing the TemplateId in the payload for updates + const updatedPayload = { + ...payload, + TemplateId: template, + Action: "Save", + }; + + updatePermissions.mutate( + { + url: "/api/ExecAppApprovalTemplate?Action=Save", + data: updatedPayload, + queryKey: "ExecAppApprovalTemplate", + }, + { + onSuccess: (data) => { + // Check if we received a valid response + const newTemplateId = data?.[0]?.TemplateId || template; + + // Refresh the data + setRefetchKey((prev) => prev + 1); + }, + } + ); + }; + + // Show error if template doesn't exist + if (!template) { + return ( + + + + The requested app approval template does not exist or was not specified. Please select a + valid template from the list. + + + + + ); + } + + return ( + + + + + + ); +}; + +// Changed from TabbedLayout to just DashboardLayout +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/templates/index.js b/src/pages/tenant/administration/applications/templates/index.js new file mode 100644 index 000000000000..c8e7e567a617 --- /dev/null +++ b/src/pages/tenant/administration/applications/templates/index.js @@ -0,0 +1,143 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Edit, Delete, ContentCopy, Add, GitHub } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Templates"; + const apiUrl = "/api/ListAppApprovalTemplates"; + + // Fetch GitHub integration status + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + icon: , + label: "Edit Template", + color: "warning", + link: "/tenant/administration/applications/templates/edit?template=[TemplateId]&name=[TemplateName]", + }, + { + icon: , + label: "Copy Template", + color: "info", + link: "/tenant/administration/applications/templates/add?template=[TemplateId]©=true&name=[TemplateName]", + }, + { + icon: , + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + data: { + Action: "UploadTemplate", + GUID: "TemplateId", + TemplateType: "AppApproval", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + icon: , + label: "Delete Template", + color: "danger", + url: "/api/ExecAppApprovalTemplate", + data: { + Action: "Delete", + TemplateId: "TemplateId", + }, + type: "POST", + confirmText: "Are you sure you want to delete [TemplateName]?", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "TemplateName", + "AppId", + "AppName", + "PermissionSetName", + "UpdatedBy", + "Timestamp", + ], + actions: actions, + }; + + const simpleColumns = [ + "TemplateName", + "AppId", + "AppName", + "PermissionSetName", + "UpdatedBy", + "Timestamp", + ]; + + return ( + } + > + Add App Approval Template + + } + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index 70939b35cf04..fab698e09464 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -80,7 +80,7 @@ const Page = () => { <> - + { )} - + { const currentTenant = useSettings().currentTenant; @@ -24,14 +26,14 @@ const Page = () => { const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); const [updatesData, setUpdatesData] = useState({ data: {}, ready: false }); const cippTableDialog = useDialog(); - + const router = useRouter(); const timeAgo = new TimeAgo("en-US"); const openRemediation = (url) => { if (url.startsWith("https")) { window.open(url, "_blank"); } else { - navigate(url); + router.push(url); } }; return ( @@ -44,7 +46,7 @@ const Page = () => { > {currentTenant === "AllTenants" && ( - + { )} {currentTenant !== "AllTenants" && ( <> - + { ]} /> - + { {currentTenant !== "AllTenants" && secureScore.isSuccess && secureScore.translatedData.controlScores.map((secureScoreControl) => ( - + { @@ -23,7 +24,7 @@ const Page = () => { > {currentTenant === "AllTenants" && ( - + { const steps = [ diff --git a/src/pages/tenant/administration/tenants/edit.js b/src/pages/tenant/administration/tenants/edit.js index ddd3290ed777..ad43de7e7811 100644 --- a/src/pages/tenant/administration/tenants/edit.js +++ b/src/pages/tenant/administration/tenants/edit.js @@ -4,7 +4,8 @@ import { ApiGetCall } from "../../../../api/ApiCall"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { Stack, Box, Tab, Tabs, Grid, Typography } from "@mui/material"; +import { Stack, Box, Tab, Tabs, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippCardTabPanel } from "/src/components/CippComponents/CippCardTabPanel"; import CippFormSection from "/src/components/CippFormPages/CippFormSection"; import CippPageCard from "../../../../components/CippCards/CippPageCard"; @@ -71,7 +72,7 @@ const Page = () => { - + { isFetching={tenantDetails.isFetching} /> - + { - + { > - + { Wizard. Backups run daily or on demand by clicking the backup now button. - + { /> - + Identity - + { formControl={formControl} /> - + - + Conditional Access - + { /> {/* Optional: Add an empty Grid item to balance the layout */} - + - + Intune - + { formControl={formControl} /> - + { formControl={formControl} /> - + { /> {/* Add an empty Grid item to fill the second column */} - + - + Email Security - + { formControl={formControl} /> - + { /> - + CIPP - + { formControl={formControl} /> - + { /> {/* Add an empty Grid item to fill the second column */} - + ); diff --git a/src/pages/tenant/backup/backup-wizard/restore.jsx b/src/pages/tenant/backup/backup-wizard/restore.jsx index d7d8c1f16c37..566d1d6857b7 100644 --- a/src/pages/tenant/backup/backup-wizard/restore.jsx +++ b/src/pages/tenant/backup/backup-wizard/restore.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Alert, Divider, Grid, Typography } from "@mui/material"; +import { Alert, Divider, Typography } 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"; @@ -107,7 +108,7 @@ const RestoreBackupForm = () => { {/* Backup Selector */} - + { {/* Restore Settings */} - + Restore Settings {/* Identity */} - + Identity { {/* Conditional Access */} - + Conditional Access { {/* Intune */} - + Intune { {/* Email Security */} - + Email Security { {/* CIPP */} - + CIPP { {/* Overwrite Existing Entries */} - + { compareType="is" compareValue={true} > - + Warning: Overwriting existing entries will remove the current settings and replace them with the backup settings. If you have selected to restore @@ -246,10 +247,10 @@ const RestoreBackupForm = () => { {/* Send Results To */} - + Send Restore results to: - + { formControl={formControl} /> - + - + - + {/* Review and Confirm */} - + Review and Confirm Please review the selected options before submitting. - + Selected Tenant: {tenantFilter} - + Selected Backup: {formControl.watch("backup")?.label || "None selected"} - + Overwrite Existing Configuration: {formControl.watch("overwrite") ? "Yes" : "No"} - + Send Results To: diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx index 5e64bb392ea3..2e3a1bad9665 100644 --- a/src/pages/tenant/conditional/deploy-vacation/add.jsx +++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Box, Divider, Grid, Typography } from "@mui/material"; +import { Box, Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; @@ -45,22 +46,24 @@ const Page = () => { exclusions for a specific period of time. Select the CA policy and the date range. - + {/* User Selector */} - + {/* Conditional Access Policy Selector */} - + { }} multiple={false} formControl={formControl} + validators={{ + validate: (option) => { + if (!option?.value) { + return "Picking a policy is required"; + } + return true; + }, + }} + required={true} /> {/* Start Date Picker */} - + { + if (!value) { + return "Start date is required"; + } + return true; + }, + }} /> {/* End Date Picker */} - + { + const startDate = formControl.getValues("startDate"); + if (!value) { + return "End date is required"; + } + if (startDate && value && new Date(value * 1000) < new Date(startDate * 1000)) { + return "End date must be after start date"; + } + return true; + }, + }} /> diff --git a/src/pages/tenant/conditional/list-named-locations/add.jsx b/src/pages/tenant/conditional/list-named-locations/add.jsx index 58691f1a29cb..17860bb8b882 100644 --- a/src/pages/tenant/conditional/list-named-locations/add.jsx +++ b/src/pages/tenant/conditional/list-named-locations/add.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Grid, Typography } from "@mui/material"; +import { Typography } 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"; @@ -31,7 +32,7 @@ const DeployNamedLocationForm = () => { - + { /> - + { /> - + { compareType="is" compareValue="IPLocation" > - + { validators={{ required: "IPs are required" }} /> - + { compareType="is" compareValue="Countries" > - + { validators={{ required: "At least one country must be selected" }} /> - + { !relationship?.addedFields?.displayName?.startsWith("MLT_") ); }, + showRefresh: true, }} multiple={false} creatable={true} diff --git a/src/pages/tenant/gdap-management/roles/add.js b/src/pages/tenant/gdap-management/roles/add.js index 50fb24b47137..cfbeecd1e855 100644 --- a/src/pages/tenant/gdap-management/roles/add.js +++ b/src/pages/tenant/gdap-management/roles/add.js @@ -1,12 +1,12 @@ import React, { useState } from "react"; -import { Alert, Button, SvgIcon, Typography, Grid, Tooltip, Link } from "@mui/material"; +import { Alert, Button, SvgIcon, Typography, Tooltip, Link } from "@mui/material"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; import GDAPRoles from "/src/data/GDAPRoles"; -import { Box, Stack } from "@mui/system"; +import { Box, Stack, Grid } from "@mui/system"; import { ShieldCheckIcon, PlusSmallIcon } from "@heroicons/react/24/outline"; import { CippPropertyList } from "/src/components/CippComponents/CippPropertyList"; import cippDefaults from "/src/data/CIPPDefaultGDAPRoles"; @@ -243,7 +243,7 @@ const Page = () => { - + { - + { sortOptions={true} /> - + - + {pageTitle} @@ -154,7 +144,7 @@ const Page = () => { {currentTenant === "AllTenants" && layoutMode !== "Table" ? ( - + { {blockCards.map((block, index) => ( { /> ) : block.formatter === "number" ? ( //really big number centered in the card. - + (
    {block.data}
    -
    +
    ) ) : block.formatter === "Percentage" ? ( <>{block.data} ) : block.formatter === "table" ? ( diff --git a/src/pages/tenant/tools/appapproval/index.js b/src/pages/tenant/tools/appapproval/index.js index 10067259d287..52e6040e62f9 100644 --- a/src/pages/tenant/tools/appapproval/index.js +++ b/src/pages/tenant/tools/appapproval/index.js @@ -38,7 +38,7 @@ const Page = () => { return ( <> { > - + { @@ -15,7 +15,7 @@ const Page = () => { - + diff --git a/src/pages/tenant/tools/individual-domains/index.js b/src/pages/tenant/tools/individual-domains/index.js index 3b209769c809..1210a6a2d6d6 100644 --- a/src/pages/tenant/tools/individual-domains/index.js +++ b/src/pages/tenant/tools/individual-domains/index.js @@ -1,4 +1,5 @@ -import { Box, Container, Grid } from "@mui/material"; +import { Box, Container } from "@mui/material"; +import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippDomainCards } from "../../../../components/CippCards/CippDomainCards"; diff --git a/src/pages/tenant/tools/tenantlookup/index.js b/src/pages/tenant/tools/tenantlookup/index.js index ff8d73863b50..45446bc26349 100644 --- a/src/pages/tenant/tools/tenantlookup/index.js +++ b/src/pages/tenant/tools/tenantlookup/index.js @@ -1,13 +1,5 @@ -import { - Box, - Button, - Container, - Grid, - Typography, - CircularProgress, - Skeleton, - Link, -} from "@mui/material"; +import { Box, Button, Container, Typography, CircularProgress, Skeleton, Link } from "@mui/material"; +import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; @@ -34,13 +26,13 @@ const Page = () => { > - + - + { required /> - + diff --git a/src/sections/dashboard/account/account-details.js b/src/sections/dashboard/account/account-details.js index c558ee2237da..a41505d1002e 100644 --- a/src/sections/dashboard/account/account-details.js +++ b/src/sections/dashboard/account/account-details.js @@ -13,7 +13,7 @@ import { TextField, Typography, } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { useMockedUser } from "../../../hooks/use-mocked-user"; const companySizeOptions = ["1-10", "11-30", "31-50", "50+"]; @@ -59,10 +59,10 @@ export const AccountDetails = (props) => { - + Settings - + { - + Change password - + { {orientation === "vertical" ? ( - + - + {content} diff --git a/src/sections/dashboard/components/stats/stats-1.js b/src/sections/dashboard/components/stats/stats-1.js index cc167847160e..c0ffad59471b 100644 --- a/src/sections/dashboard/components/stats/stats-1.js +++ b/src/sections/dashboard/components/stats/stats-1.js @@ -13,7 +13,7 @@ import { SvgIcon, Typography, } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; const data = [ { icon: ( @@ -52,7 +52,7 @@ export const Stats1 = () => ( {data.map((item) => { return ( - + ( > {data.map((item) => ( { spacing={2} > { ( {stats.map((item) => ( ({ diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index bda8d8954651..a011415698fd 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -6,9 +6,6 @@ import { MailOutline, Shield, Description, - Group, - MeetingRoom, - GroupWork, GroupOutlined, } from "@mui/icons-material"; import { Chip, Link, SvgIcon } from "@mui/material"; @@ -89,6 +86,22 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ); }; + if (cellName === "baselineOption") { + return "Download Baseline"; + } + + if (cellName === "Severity" || cellName === "logsToInclude") { + if (Array.isArray(data)) { + return isText ? data.join(", ") : renderChipList(data); + } else { + return isText ? ( + data + ) : ( + + ); + } + } + //if the cellName starts with portal_, return text, or a link with an icon if (cellName.startsWith("portal_")) { const IconComponent = portalIcons[cellName]; @@ -239,6 +252,10 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? data : ; } + if (cellName === "delegatedPrivilegeStatus") { + return data === "directTenant" ? "Direct Tenant" : "GDAP Tenant"; + } + //if the cellName is tenantFilter, return a chip with the tenant name. This can sometimes be an array, sometimes be a single item. if (cellName === "tenantFilter" || cellName === "Tenant") { //check if data is an array. @@ -668,6 +685,24 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? data : ; } + // handle autocomplete labels + if (data?.label && data?.value) { + return isText ? data.label : ; + } + + // handle array of autocomplete labels + if (Array.isArray(data) && data.length > 0 && data[0]?.label && data[0]?.value) { + return isText + ? data.map((item) => item.label).join(", ") + : renderChipList( + data.map((item) => { + return { + label: item.label, + }; + }) + ); + } + // Handle arrays of strings if (Array.isArray(data) && data.every((item) => typeof item === "string") && flatten) { // if string matches json format, parse it diff --git a/src/utils/get-cipp-validator.js b/src/utils/get-cipp-validator.js index 334bbe7dce49..3b42fb6c5101 100644 --- a/src/utils/get-cipp-validator.js +++ b/src/utils/get-cipp-validator.js @@ -19,11 +19,35 @@ export const getCippValidator = (value, type) => { return typeof value === "string" || "This is not a valid string"; case "ip": return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) || "This is not a valid IP address"; + case "ipv4cidr": + return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/.test(value) || "This is not a valid IPv4 CIDR"; + case "ipv6": + return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/.test(value) || "This is not a valid IPv6 address"; + case "ipv6cidr": + return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/.test(value) || "This is not a valid IPv6 CIDR"; + case "hostname": + return /^((\*|\~)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*|\~)?$/.test(value) || "This is not a valid hostname"; + case "sha256": + return /^[A-Fa-f0-9]{64}$/.test(value) || "This is not a valid SHA256 hash"; case "guid": return ( /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) || "This is not a valid GUID" ); + case "domain": + return /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/.test(value) || "This is not a valid domain"; + case "wildcardDomain": + return /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.\*)?$/.test(value) || /^(\*)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*)?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(value) || "This is not a valid domain pattern"; + case "wildcardUrl": + return /^(https?:\/\/)?(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.\*)?([\/\?\*][a-zA-Z0-9\-\.\~\*\/\?=&%]*)?$/.test(value) || "This is not a valid URL pattern"; + case "emailAddress": + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || "This is not a valid email address"; + case "senderEntry": + return ( + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/.test(value) || + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || + "This is not a valid domain or email address" + ); default: return true; } diff --git a/staticwebapp.config.json b/staticwebapp.config.json index e82c3b401e9e..1f57342751ca 100644 --- a/staticwebapp.config.json +++ b/staticwebapp.config.json @@ -35,17 +35,9 @@ "redirect": "/.auth/logout?post_logout_redirect_uri=/LogoutRedirect" }, { - "route": "/api/ExecSAMSetup", + "route": "/authredirect", "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] }, - { - "route": "/api/AddStandardTemplate", - "allowedRoles": ["admin"] - }, - { - "route": "/api/RemoveStandardTemplate", - "allowedRoles": ["admin"] - }, { "route": "/LogoutRedirect", "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] @@ -54,53 +46,13 @@ "route": "/404", "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] }, - { - "route": "/api/RemoveStandard", - "allowedRoles": ["admin"] - }, - { - "route": "/api/add*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/api/edit*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/api/ExecSendPush", - "allowedRoles": ["admin", "editor", "readonly"] - }, - { - "route": "/api/ExecExcludeTenant", - "allowedRoles": ["admin"] - }, - { - "route": "/api/Exec*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/api/Remove*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/cipp/*", - "allowedRoles": ["admin"] - }, - { - "route": "/tenant/standards/*", - "allowedRoles": ["admin"] - }, - { - "route": "/", - "allowedRoles": ["admin", "editor", "readonly", "reader"] - }, { "route": "/api/Public*", - "allowedRoles": ["admin", "editor", "readonly", "reader", "authenticated", "anonymous"] + "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] }, { "route": "*", - "allowedRoles": ["admin", "editor", "readonly", "reader"] + "allowedRoles": ["admin", "editor", "readonly", "authenticated"] } ], "navigationFallback": { diff --git a/yarn.lock b/yarn.lock index dcb80c7f0332..d809e0a29713 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1872,11 +1872,31 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.69.0.tgz#c434505987ade936dc53e6e27aa1406b0295516f" integrity sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ== +"@tanstack/query-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.76.0.tgz#3b4d5d34ce307ba0cf7d1a3e90d7adcdc6c46be0" + integrity sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg== + "@tanstack/query-devtools@5.67.2": version "5.67.2" resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.67.2.tgz#890ae9913bd21d3969c7fd85c68b1bd1500cfc57" integrity sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg== +"@tanstack/query-persist-client-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.76.0.tgz#a3bcdd687384dc6b5b61b402bef153ad54515321" + integrity sha512-xcTZjILf4q49Nsl6wcnhBYZ4O0gpnuNwV6vPIEWIrwTuSNWz2zd/g9bc8SxnXy7xCV8SM1H0IJn8KjLQIUb2ag== + dependencies: + "@tanstack/query-core" "5.76.0" + +"@tanstack/query-sync-storage-persister@^5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.76.0.tgz#29643062f1a424873afb22032ce70ee72436bb9b" + integrity sha512-N8d8voY61XkM+jfXTySduLrevD6wRM3pwQ1kG0syLiWWx/sX2+CpaTMSPr0GggjQuhmjhUPo83LaV+e449tizA== + dependencies: + "@tanstack/query-core" "5.76.0" + "@tanstack/query-persist-client-core" "5.76.0" + "@tanstack/react-query-devtools@^5.51.11": version "5.69.0" resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.69.0.tgz#2cb8083028aab591b9a82caf68cd7a383a0c8b1a" @@ -1884,6 +1904,13 @@ dependencies: "@tanstack/query-devtools" "5.67.2" +"@tanstack/react-query-persist-client@^5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.76.0.tgz#97718fec844708cb98a5902d4b1eeb72adea555b" + integrity sha512-QPKgkHX1yC1Ec21FTQHBTbQcHYI+6157DgsmxABp94H7/ZUJ3szZ7wdpdBPQyZ9VxBXlKRN+aNZkOPC90+r/uA== + dependencies: + "@tanstack/query-persist-client-core" "5.76.0" + "@tanstack/react-query@^5.51.11": version "5.69.0" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.69.0.tgz#8d58e800854cc11d0aa2c39569f53ae32ba442a9"