diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml deleted file mode 100644 index 01fd002acea0..000000000000 --- a/.github/workflows/dev_deploy.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: CIPP Development Frontend CI/CD - -on: - push: - branches: - - dev - -jobs: - build_and_deploy_job: - if: github.event.repository.fork == false && github.event_name == 'push' - runs-on: ubuntu-latest - name: Build and Deploy Job - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AMBITIOUS_MOSS_0A047A40F }} # change this to your repository secret name - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: 'upload' - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: '/' # App source code path - api_location: '' # Api source code path - optional - output_location: 'out' # Built app content directory - optional - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event.repository.fork == false && github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AMBITIOUS_MOSS_0A047A40F }} # change this to your repository secret name - action: 'close' diff --git a/public/assets/integrations/cloudflare.png b/public/assets/integrations/cloudflare.png new file mode 100644 index 000000000000..636ae6831405 Binary files /dev/null and b/public/assets/integrations/cloudflare.png differ diff --git a/public/version.json b/public/version.json index dba87fa5d4c8..b2e85d7083a9 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "7.0.5" + "version": "7.1.0" } diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index 41d3609a1756..aa7fc42a8bca 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -13,9 +13,10 @@ import { } from "@mui/material"; import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon"; import { CippPropertyListCard } from "./CippPropertyListCard"; +import { CippDataTable } from "../CippTable/CippDataTable"; export const CippBannerListCard = (props) => { - const { items = [], isCollapsible = false, isFetching = false, ...other } = props; + const { items = [], isCollapsible = false, isFetching = false, children, ...other } = props; const [expanded, setExpanded] = useState(null); const handleExpand = useCallback((itemId) => { @@ -113,17 +114,19 @@ export const CippBannerListCard = (props) => { {/* Right Side: Status and Expand Icon */} - - - {item.statusText} - + {item?.statusText && ( + + + {item.statusText} + + )} {isCollapsible && ( handleExpand(item.id)}> { {isCollapsible && ( - + + {item?.propertyItems?.length > 0 && ( + + )} + {item?.table && } + {item?.children && {item.children}} + {item?.actionButton && {item.actionButton}} + )} @@ -174,8 +184,12 @@ CippBannerListCard.propTypes = { subtext: PropTypes.string, statusColor: PropTypes.string, statusText: PropTypes.string, + actionButton: PropTypes.element, propertyItems: PropTypes.array, + table: PropTypes.object, + actionButton: PropTypes.element, isFetching: PropTypes.bool, + children: PropTypes.node, }) ).isRequired, isCollapsible: PropTypes.bool, diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx index a1fdb6a74199..0da5fb61de97 100644 --- a/src/components/CippCards/CippDomainCards.jsx +++ b/src/components/CippCards/CippDomainCards.jsx @@ -10,6 +10,8 @@ import { Typography, Chip, Stack, + Divider, + FormControlLabel, } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; import ClearIcon from "@mui/icons-material/Clear"; @@ -19,7 +21,7 @@ import ErrorIcon from "@mui/icons-material/Error"; import WarningIcon from "@mui/icons-material/Warning"; import HelpIcon from "@mui/icons-material/Help"; import MoreVertIcon from "@mui/icons-material/MoreVert"; -import { Controller, get, useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { ApiGetCall } from "/src/api/ApiCall"; import CippButtonCard from "/src/components/CippCards/CippButtonCard"; import { CippCodeBlock } from "/src/components/CippComponents/CippCodeBlock"; @@ -268,6 +270,60 @@ function DomainResultCard({ title, data, isFetching, info, type }) { > ), } + : type === "HTTPS" + ? { + children: ( + <> + {data?.Tests?.map((test, index) => ( + <> + dns.Unicode), + "DNSName" + ), + }, + ]} + /> + + + > + ))} + > + ), + } : {}; return ( @@ -326,6 +382,9 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) }); const [optionsVisible, setOptionsVisible] = useState(false); const [domain, setDomain] = useState(propDomain); + const [selector, setSelector] = useState(""); + const [spfRecord, setSpfRecord] = useState(""); + const [subdomains, setSubdomains] = useState(""); const enableHttps = watch("enableHttps"); useEffect(() => { @@ -337,6 +396,9 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) const onSubmit = (values) => { setDomain(values.domain); + setSelector(values.dkimSelector); + setSpfRecord(values.spfRecord); + setSubdomains(values.subdomains); }; const handleClear = () => { @@ -344,6 +406,10 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) setValue("spfRecord", ""); setValue("dkimSelector", ""); setValue("subdomains", ""); + setDomain(""); + setSelector(""); + setSpfRecord(""); + setSubdomains(""); }; // API calls with dynamic queryKey using domain @@ -370,8 +436,8 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) const { data: spfData, isFetching: spfLoading } = ApiGetCall({ url: "/api/ListDomainHealth", - queryKey: `spf-${domain}`, - data: { Domain: domain, Action: "ReadSPFRecord" }, + queryKey: `spf-${domain}-${spfRecord}`, + data: { Domain: domain, Action: "ReadSPFRecord", Record: spfRecord }, waiting: !!domain, }); @@ -384,8 +450,8 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) const { data: dkimData, isFetching: dkimLoading } = ApiGetCall({ url: "/api/ListDomainHealth", - queryKey: `dkim-${domain}`, - data: { Domain: domain, Action: "ReadDkimRecord" }, + queryKey: `dkim-${domain}-${selector}`, + data: { Domain: domain, Action: "ReadDkimRecord", Selector: selector }, waiting: !!domain, }); @@ -403,6 +469,13 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) waiting: !!domain, }); + const { data: httpsData, isFetching: httpsLoading } = ApiGetCall({ + url: "/api/ListDomainHealth", + queryKey: `https-${domain}-${subdomains}`, + data: { Domain: domain, Action: "TestHttpsCertificate", Subdomains: subdomains }, + waiting: !!domain && enableHttps, + }); + // Adjust grid item size based on fullwidth prop const gridItemSize = fullwidth ? 12 : 4; @@ -438,45 +511,50 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - {enableHttps && ( + ( - + )} /> - )} - } - onClick={handleClear} - className="mt-2" - > - Clear - + ( + + )} + /> + ( + } + label="Enable HTTPS check" + /> + )} + /> + {enableHttps && ( + ( + + )} + /> + )} + } + onClick={handleClear} + className="mt-2" + > + Clear + + @@ -605,6 +683,25 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) } /> + {enableHttps && ( + + + + + } + /> + + )} > )} diff --git a/src/components/CippCards/CippPropertyListCard.jsx b/src/components/CippCards/CippPropertyListCard.jsx index 62b4578358d6..ff7cdcd92a64 100644 --- a/src/components/CippCards/CippPropertyListCard.jsx +++ b/src/components/CippCards/CippPropertyListCard.jsx @@ -44,6 +44,12 @@ export const CippPropertyListCard = (props) => { }; const setPadding = isLabelPresent ? { py: 0.5, px: 3 } : { py: 1.5, px: 3 }; + const handleActionDisabled = (row, action) => { + if (action?.condition) { + return !action.condition(row); + } + return false; + }; return ( <> @@ -133,7 +139,7 @@ export const CippPropertyListCard = (props) => { {actionItems?.length > 0 && actionItems.map((item, index) => ( {item.icon}} label={item.label} onClick={ @@ -145,9 +151,14 @@ export const CippPropertyListCard = (props) => { action: item, ready: true, }); - createDialog.handleOpen(); + if (item?.noConfirm) { + item.customFunction(item, data, {}); + } else { + createDialog.handleOpen(); + } } } + disabled={handleActionDisabled(data, item)} /> ))} diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index c724f0ddac05..834cb3e958dc 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -9,7 +9,16 @@ import { useSettings } from "../../hooks/use-settings"; import CippFormComponent from "./CippFormComponent"; export const CippApiDialog = (props) => { - const { createDialog, title, fields, api, row = {}, relatedQueryKeys, ...other } = props; + const { + createDialog, + title, + fields, + api, + row = {}, + relatedQueryKeys, + dialogAfterEffect, + ...other + } = props; const router = useRouter(); const [addedFieldData, setAddedFieldData] = useState({}); const [partialResults, setPartialResults] = useState([]); @@ -38,6 +47,9 @@ export const CippApiDialog = (props) => { bulkRequest: api.multiPost === false, onResult: (result) => { setPartialResults((prevResults) => [...prevResults, result]); + if (api?.onSuccess) { + api.onSuccess(result); + } }, }); const actionGetRequest = ApiGetCall({ @@ -50,6 +62,9 @@ export const CippApiDialog = (props) => { bulkRequest: api.multiPost === false, onResult: (result) => { setPartialResults((prevResults) => [...prevResults, result]); + if (api?.onSuccess) { + api.onSuccess(result); + } }, }); @@ -58,8 +73,6 @@ export const CippApiDialog = (props) => { return api.dataFunction(row); } var newData = {}; - console.log("the received row", row); - console.log("the received dataObject", dataObject); if (api?.postEntireRow) { newData = row; @@ -87,7 +100,6 @@ export const CippApiDialog = (props) => { } }); } - console.log("output", newData); return newData; }; const tenantFilter = useSettings().currentTenant; @@ -132,6 +144,7 @@ export const CippApiDialog = (props) => { data: arrayOfObjects, }); } + return; } @@ -185,7 +198,12 @@ 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); + } + }, [actionPostRequest.isSuccess, actionGetRequest.isSuccess]); const formHook = useForm(); const onSubmit = (data) => handleActionClick(row, api, data); const selectedType = api.type === "POST" ? actionPostRequest : actionGetRequest; diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 1513e4ca8da1..8ecfd6aee8cd 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -1,9 +1,9 @@ import { ArrowDropDown } from "@mui/icons-material"; import { Autocomplete, CircularProgress, createFilterOptions, TextField } from "@mui/material"; -import { ApiGetCall } from "../../api/ApiCall"; import { useEffect, useState } from "react"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; +import { ApiGetCallWithPagination } from "../../api/ApiCall"; export const CippAutoComplete = (props) => { const { @@ -22,18 +22,33 @@ export const CippAutoComplete = (props) => { onChange, onCreateOption, required = false, + isFetching = false, sx, ...other } = props; - const filter = createFilterOptions(); + const [usedOptions, setUsedOptions] = useState(options); const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" }); + const filter = createFilterOptions({ + stringify: (option) => JSON.stringify(option), + }); - const actionGetRequest = ApiGetCall({ + // This is our paginated call + const actionGetRequest = ApiGetCallWithPagination({ ...getRequestInfo, }); const currentTenant = api?.tenantFilter ? api.tenantFilter : useSettings().currentTenant; + useEffect(() => { + if (actionGetRequest.isSuccess && !actionGetRequest.isFetching) { + const lastPage = actionGetRequest.data?.pages[actionGetRequest.data.pages.length - 1]; + const nextLinkExists = lastPage?.Metadata?.nextLink; + if (nextLinkExists) { + actionGetRequest.fetchNextPage(); + } + } + }, [actionGetRequest.data?.pages?.length, actionGetRequest.isFetching, api?.queryKey]); + useEffect(() => { if (api) { setGetRequestInfo({ @@ -46,52 +61,87 @@ export const CippAutoComplete = (props) => { queryKey: api.queryKey, }); } + }, [api, currentTenant]); + // After the data is fetched, combine and map it + useEffect(() => { if (actionGetRequest.isSuccess) { - const dataToMap = api.dataKey ? actionGetRequest.data?.[api.dataKey] : actionGetRequest.data; - if (!Array.isArray(dataToMap)) { + // E.g., allPages is an array of pages returned by the pagination + const allPages = actionGetRequest.data?.pages || []; + + // Helper to get nested data if you have something like "response.data.items" + const getNestedValue = (obj, path) => { + if (!path) return obj; + const keys = path.split("."); + let result = obj; + for (const key of keys) { + if (result && typeof result === "object" && key in result) { + result = result[key]; + } else { + return undefined; + } + } + return result; + }; + + // Flatten the results from all pages + const combinedResults = allPages.flatMap((page) => { + const nestedData = getNestedValue(page, api?.dataKey); + return nestedData !== undefined ? nestedData : []; + }); + + if (!Array.isArray(combinedResults)) { setUsedOptions([ { label: "Error: The API returned data we cannot map to this field", - value: "Error: The API returned data we cannot map to this field", + value: "Error", }, ]); - return; + } else { + // Convert each item into your { label, value, addedFields } shape + const convertedOptions = combinedResults.map((option) => { + const addedFields = {}; + if (api?.addedField) { + Object.keys(api.addedField).forEach((key) => { + addedFields[key] = option[api.addedField[key]]; + }); + } + + return { + label: + typeof api?.labelField === "function" + ? api.labelField(option) + : option[api?.labelField], + value: + typeof api?.valueField === "function" + ? api.valueField(option) + : option[api?.valueField], + addedFields, + }; + }); + setUsedOptions(convertedOptions); } - const convertedOptions = dataToMap.map((option) => { - const addedFields = {}; - if (api.addedField) { - Object.keys(api.addedField).forEach((key) => { - addedFields[key] = option[api.addedField[key]]; - }); - } - return { - label: - typeof api.labelField === "function" ? api.labelField(option) : option[api.labelField], - value: - typeof api.valueField === "function" ? api.valueField(option) : option[api.valueField], - addedFields: addedFields, - }; - }); - setUsedOptions(convertedOptions); } + if (actionGetRequest.isError) { setUsedOptions([{ label: getCippError(actionGetRequest.error), value: "error" }]); } - }, [api, actionGetRequest.data]); + }, [api, actionGetRequest.data, actionGetRequest.isSuccess, actionGetRequest.isError]); + const rand = Math.random().toString(36).substring(5); + return ( ) : ( ) } - isOptionEqualToValue={(option, value) => option.value === value.value} + isOptionEqualToValue={(option, val) => option.value === val.value} value={typeof value === "string" ? { label: value, value: value } : value} filterSelectedOptions disableClearable={disableClearable} @@ -100,12 +150,11 @@ export const CippAutoComplete = (props) => { filterOptions={(options, params) => { const filtered = filter(options, params); const isExisting = - options !== undefined && - options !== null && options?.length > 0 && - options?.some( + options.some( (option) => params.inputValue === option.value || params.inputValue === option.label ); + if (params.inputValue !== "" && creatable && !isExisting) { filtered.push({ label: `Add option: "${params.inputValue}"`, @@ -126,6 +175,7 @@ export const CippAutoComplete = (props) => { onChange={(event, newValue) => { if (Array.isArray(newValue)) { newValue = newValue.map((item) => { + // If user typed a new item or missing label if (item?.manual || !item?.label) { item = { label: item?.label ? item.value : item, diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx new file mode 100644 index 000000000000..c710a8d2d49b --- /dev/null +++ b/src/components/CippComponents/CippCentralSearch.jsx @@ -0,0 +1,140 @@ +import React, { useState } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, + Grid, + Card, + CardContent, + Typography, + Box, +} from "@mui/material"; +import { useRouter } from "next/router"; +import { nativeMenuItems } from "/src/layouts/config"; + +/** + * Recursively collects only leaf items (those without sub-items). + * If an item has an `items` array, we skip that item itself (treat it as a "folder") + * and continue flattening deeper. + */ +function getLeafItems(items = []) { + let result = []; + + for (const item of items) { + if (item.items && Array.isArray(item.items) && item.items.length > 0) { + // recurse into children + result = result.concat(getLeafItems(item.items)); + } else { + // no child items => this is a leaf + result.push(item); + } + } + + return result; +} + +export const CippCentralSearch = ({ handleClose, open }) => { + const router = useRouter(); + const [searchValue, setSearchValue] = useState(""); + + // Flatten the menu items once + const flattenedMenuItems = getLeafItems(nativeMenuItems); + + const handleChange = (event) => { + setSearchValue(event.target.value); + }; + + // Optionally handle Enter key + const handleKeyDown = (event) => { + if (event.key === "Enter") { + // do something if needed, e.g., analytics or highlight + } + }; + + // Filter leaf items by matching title or path + const normalizedSearch = searchValue.trim().toLowerCase(); + const filteredItems = flattenedMenuItems.filter((leaf) => { + const inTitle = leaf.title?.toLowerCase().includes(normalizedSearch); + const inPath = leaf.path?.toLowerCase().includes(normalizedSearch); + // If there's no search value, show no results (you could change this logic) + return normalizedSearch ? inTitle || inPath : false; + }); + + // Helper to boldâhighlight the matched text + const highlightMatch = (text = "") => { + if (!normalizedSearch) return text; + const parts = text.split(new RegExp(`(${normalizedSearch})`, "gi")); + return parts.map((part, i) => + part.toLowerCase() === normalizedSearch ? ( + + {part} + + ) : ( + part + ) + ); + }; + + // Click handler: shallow navigate with Next.js + const handleCardClick = (path) => { + router.push(path, undefined, { shallow: true }); + handleClose(); + }; + + return ( + + CIPP Search + + + + + + {/* Show results or "No results" */} + {searchValue.trim().length > 0 ? ( + filteredItems.length > 0 ? ( + + {filteredItems.map((item, index) => ( + + handleCardClick(item.path)} + > + + {highlightMatch(item.title)} + + Path: {highlightMatch(item.path)} + + + + + ))} + + ) : ( + No results found. + ) + ) : ( + + Type something to search by title or path. + + )} + + + + + Close + + + + ); +}; diff --git a/src/components/CippComponents/CippLocationDialog.jsx b/src/components/CippComponents/CippLocationDialog.jsx new file mode 100644 index 000000000000..403f15891395 --- /dev/null +++ b/src/components/CippComponents/CippLocationDialog.jsx @@ -0,0 +1,52 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { useState } from "react"; +import dynamic from "next/dynamic"; // Import dynamic from next/dynamic +import { CippPropertyList } from "./CippPropertyList"; // Import CippPropertyList +import { LocationOn } from "@mui/icons-material"; + +const CippMap = dynamic(() => import("./CippMap"), { ssr: false }); // Dynamic import for CippMap + +export const CippLocationDialog = ({ location }) => { + const [open, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const markers = [ + { + position: [location.geoCoordinates.latitude, location.geoCoordinates.longitude], + popup: `${location.city}, ${location.state}, ${location.countryOrRegion}`, + }, + ]; + + const properties = [ + { label: "City", value: location.city }, + { label: "State", value: location.state }, + { label: "Country/Region", value: location.countryOrRegion }, + ]; + + return ( + <> + }> + {location.city}, {location.state}, {location.countryOrRegion} + + + Location Details + + + + + + + Close + + + + > + ); +}; diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index dade5cfd0d6b..0876bded5bd2 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -42,4 +42,6 @@ export const CippTranslations = { ToIP: "To IP", "info.logoUrl": "Logo", "commitmentTerm.renewalConfiguration.renewalDate": "Renewal Date", + storageUsedInBytes: "Storage Used", + prohibitSendReceiveQuotaInBytes: "Quota", }; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx new file mode 100644 index 000000000000..fe532d259f4b --- /dev/null +++ b/src/components/CippComponents/CippUserActions.jsx @@ -0,0 +1,317 @@ +import { EyeIcon, MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { + Archive, + Block, + Clear, + CloudDone, + Edit, + Email, + ForwardToInbox, + GroupAdd, + LockOpen, + LockPerson, + LockReset, + MeetingRoom, + NoMeetingRoom, + Password, + PersonOff, + PhonelinkLock, + PhonelinkSetup, + Shortcut, +} from "@mui/icons-material"; +import { useSettings } from "/src/hooks/use-settings.js"; + +export const CippUserActions = () => { + const tenant = useSettings().currentTenant; + return [ + { + //tested + label: "View User", + link: "/identity/administration/users/user?userId=[id]", + multiPost: false, + icon: , + color: "success", + }, + { + //tested + label: "Edit User", + link: "/identity/administration/users/user/edit?userId=[id]", + icon: , + color: "success", + target: "_self", + }, + { + //tested + label: "Research Compromised Account", + type: "GET", + icon: , + link: "/identity/administration/users/user/bec?userId=[id]", + confirmText: "Are you sure you want to research this compromised account?", + multiPost: false, + }, + { + //tested + + label: "Create Temporary Access Password", + type: "GET", + icon: , + url: "/api/ExecCreateTAP", + data: { ID: "userPrincipalName" }, + confirmText: "Are you sure you want to create a Temporary Access Password?", + multiPost: false, + }, + { + //tested + label: "Re-require MFA registration", + type: "GET", + icon: , + url: "/api/ExecResetMFA", + data: { ID: "userPrincipalName" }, + confirmText: "Are you sure you want to reset MFA for this user?", + multiPost: false, + }, + { + //tested + label: "Send MFA Push", + type: "POST", + icon: , + url: "/api/ExecSendPush", + data: { UserEmail: "userPrincipalName" }, + confirmText: "Are you sure you want to send an MFA request?", + multiPost: false, + }, + { + //tested + label: "Set Per-User MFA", + type: "POST", + icon: , + url: "/api/ExecPerUserMFA", + data: { userId: "userPrincipalName" }, + fields: [ + { + type: "autoComplete", + name: "State", + label: "State", + options: [ + { label: "Enforced", value: "Enforced" }, + { label: "Enabled", value: "Enabled" }, + { label: "Disabled", value: "Disabled" }, + ], + multiple: false, + }, + ], + confirmText: "Are you sure you want to set per-user MFA for these users?", + multiPost: false, + }, + { + //tested + label: "Convert to Shared Mailbox", + type: "GET", + icon: , + url: "/api/ExecConvertToSharedMailbox", + data: { ID: "userPrincipalName" }, + confirmText: "Are you sure you want to convert this user to a shared mailbox?", + multiPost: false, + }, + { + label: "Convert to User Mailbox", + type: "GET", + icon: , + url: "/api/ExecConvertToSharedMailbox", + data: { ID: "userPrincipalName", ConvertToUser: true }, + confirmText: "Are you sure you want to convert this user to a user mailbox?", + multiPost: false, + }, + { + //tested + label: "Enable Online Archive", + type: "GET", + icon: , + url: "/api/ExecEnableArchive", + data: { ID: "userPrincipalName" }, + confirmText: "Are you sure you want to enable the online archive for this user?", + multiPost: false, + }, + { + //tested + label: "Set Out of Office", + type: "POST", + icon: , + url: "/api/ExecSetOoO", + data: { + userId: "userPrincipalName", + AutoReplyState: { value: "Enabled" }, + tenantFilter: "Tenant", + }, + fields: [{ type: "richText", name: "input", label: "Out of Office Message" }], + confirmText: "Are you sure you want to set the out of office?", + multiPost: false, + }, + + { + label: "Disable Out of Office", + type: "POST", + icon: , + url: "/api/ExecSetOoO", + data: { user: "userPrincipalName", AutoReplyState: "Disabled" }, + confirmText: "Are you sure you want to disable the out of office?", + multiPost: false, + }, + { + label: "Add to Group", + type: "POST", + icon: , + url: "/api/EditGroup", + data: { addMember: "userPrincipalName" }, + fields: [ + { + type: "autoComplete", + name: "groupId", + label: "Select a group to add the user to", + multiple: false, + creatable: false, + api: { + url: "/api/ListGroups", + labelField: "displayName", + valueField: "id", + addedField: { + groupType: "calculatedGroupType", + groupName: "displayName", + }, + queryKey: `groups-${tenant}`, + }, + }, + ], + confirmText: "Are you sure you want to add the user to this group?", + }, + { + label: "Disable Email Forwarding", + type: "POST", + url: "/api/ExecEmailForward", + icon: , + data: { + username: "userPrincipalName", + userid: "userPrincipalName", + ForwardOption: "!disabled", + }, + confirmText: "Are you sure you want to disable forwarding of this user's emails?", + multiPost: false, + }, + { + label: "Pre-provision OneDrive", + type: "POST", + icon: , + url: "/api/ExecOneDriveProvision", + data: { UserPrincipalName: "userPrincipalName" }, + confirmText: "Are you sure you want to pre-provision OneDrive for this user?", + multiPost: false, + }, + { + label: "Add OneDrive Shortcut", + type: "POST", + icon: , + url: "/api/ExecOneDriveShortCut", + data: { + username: "userPrincipalName", + userid: "id", + }, + fields: [ + { + type: "autoComplete", + name: "siteUrl", + label: "Select a Site", + multiple: false, + creatable: false, + api: { + url: "/api/ListSites", + data: { type: "SharePointSiteUsage", URLOnly: true }, + labelField: "webUrl", + valueField: "webUrl", + queryKey: `sharepointSites-${tenant}`, + }, + }, + ], + confirmText: "Select a SharePoint site to create a shortcut for:", + multiPost: false, + }, + { + label: "Block Sign In", + type: "GET", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "id" }, + confirmText: "Are you sure you want to block the sign-in for this user?", + multiPost: false, + condition: (row) => row.accountEnabled, + }, + { + label: "Unblock Sign In", + type: "GET", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "id", Enable: true }, + confirmText: "Are you sure you want to unblock sign-in for this user?", + multiPost: false, + condition: (row) => !row.accountEnabled, + }, + { + label: "Reset Password (Must Change)", + type: "GET", + icon: , + url: "/api/ExecResetPass", + data: { + MustChange: true, + ID: "userPrincipalName", + displayName: "displayName", + }, + confirmText: + "Are you sure you want to reset the password for this user? The user must change their password at next logon.", + multiPost: false, + }, + { + label: "Reset Password", + type: "GET", + icon: , + url: "/api/ExecResetPass", + data: { + MustChange: false, + ID: "userPrincipalName", + displayName: "displayName", + }, + confirmText: "Are you sure you want to reset the password for this user?", + multiPost: false, + }, + { + label: "Clear Immutable ID", + type: "GET", + icon: , + url: "/api/ExecClrImmId", + data: { + ID: "id", + }, + confirmText: "Are you sure you want to clear the Immutable ID for this user?", + multiPost: false, + condition: (row) => row.onPremisesSyncEnabled, + }, + { + label: "Revoke all user sessions", + type: "GET", + icon: , + url: "/api/ExecRevokeSessions", + data: { ID: "id", Username: "userPrincipalName" }, + confirmText: "Are you sure you want to revoke all sessions for this user?", + multiPost: false, + }, + { + label: "Delete User", + type: "GET", + icon: , + url: "/api/RemoveUser", + data: { ID: "id" }, + confirmText: "Are you sure you want to delete this user?", + multiPost: false, + }, + ]; +}; + +export default CippUserActions; diff --git a/src/components/CippFormPages/CippAddGroupTemplateForm.jsx b/src/components/CippFormPages/CippAddGroupTemplateForm.jsx index 8e638142c9fe..a9362038e9d0 100644 --- a/src/components/CippFormPages/CippAddGroupTemplateForm.jsx +++ b/src/components/CippFormPages/CippAddGroupTemplateForm.jsx @@ -13,6 +13,7 @@ const CippAddGroupTemplateForm = (props) => { type="textField" label="Display Name" name="displayName" + required formControl={formControl} fullWidth /> diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index 3049dba77660..e18e9c57421a 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -120,7 +120,12 @@ const CippSchedulerForm = (props) => { )} - + diff --git a/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx b/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx index 9157364fe66b..8b46afd209f7 100644 --- a/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx +++ b/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx @@ -44,7 +44,7 @@ const CippIntegrationFieldMapping = () => { var missingMappings = []; fieldMapping?.data?.Mappings?.forEach((mapping) => { const exists = fieldMapping?.data?.IntegrationFields?.some( - (integrationField) => integrationField.value === mapping.IntegrationId + (integrationField) => String(integrationField.value) === mapping.IntegrationId ); if (exists) { newMappings[mapping.RowKey] = { diff --git a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx index 8c3c9fd15aff..4461db5a8bc9 100644 --- a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx +++ b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx @@ -81,10 +81,10 @@ const CippIntegrationSettings = ({ children }) => { const selectedTenant = formControl.getValues("tenantFilter"); const selectedCompany = formControl.getValues("integrationCompany"); if (!selectedTenant || !selectedCompany) return; - if (tableData?.find((item) => item.TenantId === selectedTenant.value)) return; + if (tableData?.find((item) => item.TenantId === selectedTenant.addedFields.customerId)) return; const newRowData = { - TenantId: selectedTenant.value, + TenantId: selectedTenant.addedFields.customerId, Tenant: selectedTenant.label, IntegrationName: selectedCompany.label, IntegrationId: selectedCompany.value, diff --git a/src/components/CippSettings/CippGDAPResults.jsx b/src/components/CippSettings/CippGDAPResults.jsx index 99820ad401eb..c86c24939417 100644 --- a/src/components/CippSettings/CippGDAPResults.jsx +++ b/src/components/CippSettings/CippGDAPResults.jsx @@ -115,7 +115,7 @@ export const CippGDAPResults = (props) => { ) : ( <> - {gdapTests.map((test) => { + {gdapTests.map((test, index) => { var matchedResults = results?.Results?.[test.resultProperty]?.filter((item) => new RegExp(test.match)?.test(item?.[test.matchProperty]) ); @@ -128,7 +128,7 @@ export const CippGDAPResults = (props) => { } return ( - + {testResult ? : } diff --git a/src/components/CippSettings/CippPermissionCheck.jsx b/src/components/CippSettings/CippPermissionCheck.jsx index 553712441188..5ad0f5d0a502 100644 --- a/src/components/CippSettings/CippPermissionCheck.jsx +++ b/src/components/CippSettings/CippPermissionCheck.jsx @@ -1,10 +1,20 @@ -import { Box, Button, Chip, Stack, SvgIcon, Typography } from "@mui/material"; +import { + Alert, + Box, + Button, + Chip, + Collapse, + IconButton, + Stack, + SvgIcon, + Typography, +} from "@mui/material"; import CippButtonCard from "/src/components/CippCards/CippButtonCard"; import { ApiGetCall } from "/src/api/ApiCall"; import { useEffect, useState } from "react"; import { CippPermissionResults } from "./CippPermissionResults"; import { CippGDAPResults } from "./CippGDAPResults"; -import { Sync } from "@mui/icons-material"; +import { Close, Sync } from "@mui/icons-material"; import { CippTenantResults } from "./CippTenantResults"; import { CippTimeAgo } from "../CippComponents/CippTimeAgo"; import { Description } from "@mui/icons-material"; @@ -14,6 +24,7 @@ const CippPermissionCheck = (props) => { const [skipCache, setSkipCache] = useState(false); const [cardIcon, setCardIcon] = useState(null); const [offcanvasVisible, setOffcanvasVisible] = useState(false); + const [showAlertMessage, setShowAlertMessage] = useState(true); var showDetails = true; if (type === "Tenants") { @@ -130,6 +141,26 @@ const CippPermissionCheck = (props) => { > {(executeCheck.isSuccess || executeCheck.isLoading) && ( <> + {executeCheck.data?.Metadata?.AlertMessage && ( + + setShowAlertMessage(false)} + > + + + } + > + {executeCheck.data.Metadata.AlertMessage} + + + )} {type === "Permissions" && ( { const [currentStep, setCurrentStep] = useState(0); + const [savedItem, setSavedItem] = useState(null); + const dialogAfterEffect = (id) => { + setSavedItem(id); + }; const watchForm = useWatch({ control: formControl.control }); @@ -208,6 +212,7 @@ const CippStandardsSideBar = ({ dialogAfterEffect(data.id)} createDialog={createDialog} title="Add Standard" api={{ @@ -223,12 +228,17 @@ const CippStandardsSideBar = ({ templateName: "templateName", standards: "standards", ...(edit ? { GUID: "GUID" } : {}), + ...(savedItem ? { GUID: savedItem } : {}), runManually: "runManually", }, }} row={formControl.getValues()} formControl={formControl} - relatedQueryKeys={"listStandardTemplates"} + relatedQueryKeys={[ + "listStandardTemplates", + "listStandards", + `listStandardTemplates-${watchForm.GUID}`, + ]} /> ); diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 92ee885a364c..12619a983c68 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -73,7 +73,10 @@ export const CIPPTableToptoolbar = ({ //useEffect to set the column visibility to the preferred columns if they exist useEffect(() => { - if (settings?.columnDefaults?.[pageName]) { + if ( + settings?.columnDefaults?.[pageName] && + Object.keys(settings?.columnDefaults?.[pageName]).length > 0 + ) { setColumnVisibility(settings?.columnDefaults?.[pageName]); } }, [settings?.columnDefaults?.[pageName], router, usedColumns]); @@ -88,27 +91,42 @@ export const CIPPTableToptoolbar = ({ }); const resetToDefaultVisibility = () => { - setColumnVisibility({}); + setColumnVisibility((prevVisibility) => { + const updatedVisibility = {}; + for (const col in prevVisibility) { + if (Array.isArray(originalSimpleColumns)) { + updatedVisibility[col] = originalSimpleColumns.includes(col); + } + } + return updatedVisibility; + }); settings.handleUpdate({ columnDefaults: { ...settings?.columnDefaults, [pageName]: {}, }, }); + columnPopover.handleClose(); }; const resetToPreferedVisibility = () => { - if (settings?.columnDefaults?.[pageName]) { + if ( + settings?.columnDefaults?.[pageName] && + Object.keys(settings?.columnDefaults?.[pageName]).length > 0 + ) { setColumnVisibility(settings?.columnDefaults?.[pageName]); } else { setColumnVisibility((prevVisibility) => { const updatedVisibility = {}; for (const col in prevVisibility) { - updatedVisibility[col] = originalSimpleColumns.includes(col); + if (Array.isArray(originalSimpleColumns)) { + updatedVisibility[col] = originalSimpleColumns.includes(col); + } } return updatedVisibility; }); } + columnPopover.handleClose(); }; const saveAsPreferedColumns = () => { @@ -118,6 +136,7 @@ export const CIPPTableToptoolbar = ({ [pageName]: columnVisibility, }, }); + columnPopover.handleClose(); }; const mergeCaseInsensitive = (obj1, obj2) => { @@ -267,7 +286,6 @@ export const CIPPTableToptoolbar = ({ } else if (data && !getRequestData.isFetched) { //do nothing because data was sent native. } else if (getRequestData) { - console.log(getRequestData); getRequestData.refetch(); } }} @@ -460,7 +478,7 @@ export const CIPPTableToptoolbar = ({ }} > {actions - ?.filter((action) => !action.link) + ?.filter((action) => !action.link && !action?.hideBulk) .map((action, index) => ( { cardButton, offCanvas = false, noCard = false, + hideTitle = false, refreshFunction, incorrectDataMessage = "Data not in correct format", onChange, @@ -156,6 +157,13 @@ export const CippDataTable = (props) => { const memoizedColumns = useMemo(() => usedColumns, [usedColumns]); const memoizedData = useMemo(() => usedData, [usedData]); + const handleActionDisabled = (row, action) => { + if (action?.condition) { + return !action.condition(row); + } + return false; + }; + const table = useMaterialReactTable({ mrtTheme: (theme) => ({ baseBackgroundColor: theme.palette.background.paper, @@ -173,7 +181,6 @@ export const CippDataTable = (props) => { ) : undefined, onColumnVisibilityChange: setColumnVisibility, ...modeInfo, - renderRowActionMenuItems: actions ? ({ closeMenu, row }) => [ actions.map((action, index) => ( @@ -195,6 +202,7 @@ export const CippDataTable = (props) => { closeMenu(); } }} + disabled={handleActionDisabled(row.original, action)} > {action.icon} @@ -240,6 +248,7 @@ export const CippDataTable = (props) => { table={table} api={api} queryKey={queryKey} + simpleColumns={simpleColumns} data={data} columnVisibility={columnVisibility} getRequestData={getRequestData} @@ -291,8 +300,12 @@ export const CippDataTable = (props) => { ) : ( // Render the table inside a Card - - + {cardButton || !hideTitle ? ( + <> + + + > + ) : null} {!Array.isArray(usedData) && usedData ? ( diff --git a/src/components/CippTable/util-columnsFromAPI.js b/src/components/CippTable/util-columnsFromAPI.js index 0d04d20d0800..c5084c988110 100644 --- a/src/components/CippTable/util-columnsFromAPI.js +++ b/src/components/CippTable/util-columnsFromAPI.js @@ -2,18 +2,29 @@ import { getCippFilterVariant } from "../../utils/get-cipp-filter-variant"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; import { getCippTranslation } from "../../utils/get-cipp-translation"; +const skipRecursion = ["location"]; // Function to merge keys from all objects in the array const mergeKeys = (dataArray) => { return dataArray.reduce((acc, item) => { const mergeRecursive = (obj, base = {}) => { - Object?.keys(obj)?.forEach((key) => { - // If base[key] is a string, it should not be merged as an object - if (typeof base[key] === "string") { - return; // Skip further merging for this key - } - - if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) { - base[key] = mergeRecursive(obj[key], base[key] || {}); + Object.keys(obj).forEach((key) => { + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) && + !skipRecursion.includes(key) + ) { + if (typeof base[key] === "boolean") { + // Skip merging if base[key] is a boolean + return; + } + if (typeof base[key] !== "object" || Array.isArray(base[key])) { + // Re-initialize base[key] if it's not an object + base[key] = {}; + } + base[key] = mergeRecursive(obj[key], base[key]); + } else if (typeof obj[key] === "boolean") { + base[key] = obj[key]; } else if (typeof obj[key] === "string" && obj[key].toUpperCase() === "FAILED") { base[key] = base[key]; // Keep existing value if it's 'FAILED' } else if (obj[key] !== undefined && obj[key] !== null) { @@ -34,7 +45,12 @@ export const utilColumnsFromAPI = (dataArray) => { return Object.keys(obj) .map((key) => { const accessorKey = parentKey ? `${parentKey}.${key}` : key; - if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) { + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) && + !skipRecursion.includes(key) + ) { return generateColumns(obj[key], accessorKey); } diff --git a/src/components/CippWizard/CippWizardCSVImport.jsx b/src/components/CippWizard/CippWizardCSVImport.jsx index dade62abe5d9..6f665df4523b 100644 --- a/src/components/CippWizard/CippWizardCSVImport.jsx +++ b/src/components/CippWizard/CippWizardCSVImport.jsx @@ -43,7 +43,6 @@ export const CippWizardCSVImport = (props) => { const handleAddItem = () => { const newRowData = formControl.getValues("addrow"); if (newRowData === undefined) return false; - const newTableData = [...tableData, newRowData]; setTableData(newTableData); setOpen(false); @@ -87,6 +86,15 @@ export const CippWizardCSVImport = (props) => { label={getCippTranslation(field)} type="textField" formControl={formControl} + onKeyDown={(e) => { + if (e.key === "Enter") { + if (e.target.value === "") return false; + handleAddItem(); + setTimeout(() => { + formControl.setValue(`addrow.${field}`, ""); + }, 500); + } + }} /> > diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 7bdb55f719d2..0110e7d63729 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -334,7 +334,7 @@ "logoDark": "/assets/integrations/pwpush_dark.png", "forceSyncButton": false, "description": "Enable the PasswordPusher integration to generate password links instead of plain text passwords.", - "helpText": "This integration allows you to generate password links instead of plain text passwords. Configure authentication and expiration settings that will apply to all generated passwords.", + "helpText": "This integration allows you to generate password links instead of plain text passwords. Configure authentication and expiration settings that will apply to all generated passwords. If you are a PWPush Pro customer and utilizing custom domains, please do not enter your custom domain in the Base URL field.", "links": [ { "name": "PWPush Documentation", @@ -423,5 +423,45 @@ } ], "mappingRequired": false + }, + { + "name": "CloudFlare ZTNA Tunnel", + "id": "CTZTNA", + "type": "ztna", + "cat": "Zero Trust Network Tunnel", + "logo": "/assets/integrations/cloudflare.png", + "forceSyncButton": false, + "hideTestButton": true, + "description": "Enter your CloudFlare ZTNA Tunnel API Credentials to use with other Integrations", + "helpText": "These credentials can be used to allow traffic through a CloudFlare ZTNA Tunnel if your other Integrations are protected behind one. You need to select 'Behind a CF-ZTNA Tunnel' within the other Integration's settings to enable.", + "links": [ + { + "name": "CloudFlare ZTNA Tunnel Information", + "url": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" + }, + { + "name": "Installing Hudu behind Cloudflare Zero Trust Tunnel", + "url": "https://support.hudu.com/hc/en-us/articles/13929358997399-Installing-Hudu-behind-Cloudflare-Zero-Trust-Tunnel" + } + ], + "SettingOptions": [ + { + "type": "password", + "name": "CloudFlareClientID.APIKey", + "label": "CloudFlare Tunnel Service Account Client ID" + }, + { + "type": "password", + "name": "CloudFlareAPIKey.APIKey", + "label": "CloudFlare Tunnel Service Account Client Secret" + }, + { + "_comment": "I have added this switch as a logic check for the Hudu integration script to check against when CIPP first connects to the Hudu Instance via Connect-HuduAPI.ps1", + "type": "switch", + "name": "Hudu.CFEnabled", + "label": " Connect to HUDU through CloudFlare Tunnel with the above Service Account credentials." + } + ], + "mappingRequired": false } ] diff --git a/src/data/GDAPRoles.json b/src/data/GDAPRoles.json index bf14e31159e5..22553236b533 100644 --- a/src/data/GDAPRoles.json +++ b/src/data/GDAPRoles.json @@ -455,22 +455,6 @@ "Name": "Office Apps Administrator", "ObjectId": "2b745bdf-0803-4d80-aa65-822c4493daac" }, - { - "ExtensionData": {}, - "Description": "Do not use - not intended for general use.", - "IsEnabled": true, - "IsSystem": true, - "Name": "Partner Tier1 Support", - "ObjectId": "4ba39ca4-527c-499a-b93d-d9b492c50246" - }, - { - "ExtensionData": {}, - "Description": "Do not use - not intended for general use.", - "IsEnabled": true, - "IsSystem": true, - "Name": "Partner Tier2 Support", - "ObjectId": "e00e864a-17c5-4a4b-9c06-f5b95a8d5bd8" - }, { "ExtensionData": {}, "Description": "Can reset passwords for non-administrators and Password Administrators.", diff --git a/src/data/alerts.json b/src/data/alerts.json index e503a0bbf800..d027afab9869 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -22,12 +22,17 @@ { "name": "InactiveLicensedUsers", "label": "Alert on licensed users that have not logged in for 90 days", + "requiresInput": true, + "inputType": "switch", + "inputLabel": "Exclude disabled users?", + "inputName": "InactiveLicensedUsersExcludeDisabled", "recommendedRunInterval": "1d" }, { "name": "QuotaUsed", "label": "Alert on % mailbox quota used", "requiresInput": true, + "inputType": "textField", "inputLabel": "Enter quota percentage", "inputName": "QuotaUsedQuota", "recommendedRunInterval": "4h" @@ -36,8 +41,9 @@ "name": "SharePointQuota", "label": "Alert on % SharePoint quota used", "requiresInput": true, + "inputType": "textField", "inputLabel": "Enter quota percentage", - "inputName": "SharePointQuotaQuota", + "inputName": "SharePointQuota", "recommendedRunInterval": "4h" }, { @@ -85,6 +91,11 @@ "label": "Alert on new Apple Business Manager terms", "recommendedRunInterval": "30d" }, + { + "name": "AppCertificateExpiry ", + "label": "Alert on expiring application certificates", + "recommendedRunInterval": "1d" + }, { "name": "ApnCertExpiry", "label": "Alert on expiring APN certificates", diff --git a/src/data/signinErrorCodes.json b/src/data/signinErrorCodes.json new file mode 100644 index 000000000000..a4d279753b9a --- /dev/null +++ b/src/data/signinErrorCodes.json @@ -0,0 +1,17 @@ +{ + "0": "Success", + "50126": "Invalid username or password", + "70044": "The session has expired or is invalid due to sign-in frequency checks by conditional access", + "50089": "Flow token expired", + "53003": "Access has been blocked by Conditional Access policies", + "50140": "This error occurred due to 'Keep me signed in' interrupt when the user was signing-in", + "50097": "Device authentication required", + "65001": "Application X doesn't have permission to access application Y or the permission has been revoked", + "50053": "Account is locked because user tried to sign in too many times with an incorrect user ID or password", + "50020": "The user is unauthorized", + "50125": "Sign-in was interrupted due to a password reset or password registration entry", + "50074": "User did not pass the MFA challenge", + "50133": "Session is invalid due to expiration or recent password change", + "530002": "Your device is required to be compliant to access this resource", + "9001011": "Device policy contains unsupported required device state" +} \ No newline at end of file diff --git a/src/data/standards.json b/src/data/standards.json index c50f45b20e68..4a7a5527ae36 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -49,6 +49,36 @@ "powershellEquivalent": "Enable-OrganizationCustomization", "recommendedBy": ["CIS"] }, + { + "name": "standards.ProfilePhotos", + "cat": "Global Standards", + "tag": ["lowimpact"], + "helpText": "Controls whether users can set their own profile photos in Microsoft 365.", + "docsDescription": "Controls whether users can set their own profile photos in Microsoft 365. When disabled, only User and Global administrators can update profile photos for users.", + "addedComponent": [ + { + "type": "select", + "multiple": false, + "label": "Select value", + "name": "standards.ProfilePhotos.state", + "options": [ + { + "label": "Enabled", + "value": "enabled" + }, + { + "label": "Disabled", + "value": "disabled" + } + ] + } + ], + "label": "Allow users to set profile photos", + "impact": "Low Impact", + "impactColour": "info", + "powershellEquivalent": "Set-OrganizationConfig -ProfilePhotoOptions EnablePhotos and Update-MgBetaAdminPeople", + "recommendedBy": [] + }, { "name": "standards.PhishProtection", "cat": "Global Standards", @@ -644,6 +674,30 @@ "powershellEquivalent": "", "recommendedBy": [] }, + { + "name": "standards.StaleEntraDevices", + "cat": "Entra (AAD) Standards", + "tag": ["highimpact", "CIS"], + "helpText": "Cleans up Entra devices that have not connected/signed in for the specified number of days.", + "docsDescription": "Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)", + "addedComponent": [ + { + "type": "number", + "name": "standards.StaleEntraDevices.deviceAgeThreshold", + "label": "Days before stale(Dont set below 30)" + } + ], + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": true + }, + "label": "Cleanup stale Entra devices", + "impact": "High Impact", + "impactColour": "danger", + "powershellEquivalent": "Remove-MgDevice, Update-MgDevice or Graph API", + "recommendedBy": [] + }, { "name": "standards.UndoOauth", "cat": "Entra (AAD) Standards", @@ -1270,7 +1324,13 @@ "tag": ["mediumimpact"], "helpText": "Sets emails sent as and on behalf of shared mailboxes to also be stored in the shared mailbox sent items folder", "docsDescription": "This makes sure that e-mails sent from shared mailboxes or delegate mailboxes, end up in the mailbox of the shared/delegate mailbox instead of the sender, allowing you to keep replies in the same mailbox as the original e-mail.", - "addedComponent": [], + "addedComponent": [ + { + "type": "switch", + "label": "Include user mailboxes", + "name": "standards.DelegateSentItems.IncludeUserMailboxes" + } + ], "label": "Set mailbox Sent Items delegation (Sent items for shared mailboxes)", "impact": "Medium Impact", "impactColour": "warning", diff --git a/src/layouts/config.js b/src/layouts/config.js index 33e0bb77e348..d9685ba29de4 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -236,7 +236,6 @@ export const nativeMenuItems = [ { title: "Autopilot Devices", path: "/endpoint/autopilot/list-devices" }, { title: "Add Autopilot Device", path: "/endpoint/autopilot/add-device" }, { title: "Profiles", path: "/endpoint/autopilot/list-profiles" }, - { title: "Add Profile", path: "/endpoint/autopilot/add-profile" }, { title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" }, { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" }, ], @@ -377,6 +376,10 @@ export const nativeMenuItems = [ title: "Shared Mailbox with Enabled Account", path: "/email/reports/SharedMailboxEnabledAccount", }, + { + title: "Global Address List", + path: "/email/reports/global-address-list", + }, ], }, ], @@ -421,14 +424,14 @@ export const nativeMenuItems = [ { title: "Message Viewer", path: "/email/tools/message-viewer" }, ], }, - // { - // title: "Dark Web Tools", - // path: "/tools/darkweb", - // items: [ - // { title: "Tenant Breach Lookup", path: "/tools/tenantbreachlookup" }, - // { title: "Breach Lookup", path: "/tools/breachlookup" }, - // ], - // }, + { + title: "Dark Web Tools", + path: "/tools/darkweb", + items: [ + { title: "Tenant Breach Lookup", path: "/tools/tenantbreachlookup" }, + { title: "Breach Lookup", path: "/tools/breachlookup" }, + ], + }, { title: "Template Library", path: "/tools/templatelib", @@ -469,6 +472,11 @@ export const nativeMenuItems = [ path: "/cipp/advanced/timers", roles: ["superadmin"], }, + { + title: "Table Maintenance", + path: "/cipp/advanced/table-maintenance", + roles: ["superadmin"], + }, ], }, ], diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index cc229bcd9daf..b92b1ace3d31 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import NextLink from "next/link"; import PropTypes from "prop-types"; import Bars3Icon from "@heroicons/react/24/outline/Bars3Icon"; @@ -11,9 +11,13 @@ import { paths } from "../paths"; import { AccountPopover } from "./account-popover"; import { CippTenantSelector } from "../components/CippComponents/CippTenantSelector"; import { NotificationsPopover } from "./notifications-popover"; +import { useDialog } from "../hooks/use-dialog"; +import { MagnifyingGlassCircleIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { CippCentralSearch } from "../components/CippComponents/CippCentralSearch"; const TOP_NAV_HEIGHT = 64; export const TopNav = (props) => { + const searchDialog = useDialog(); const { onNavOpen } = props; const settings = useSettings(); const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -24,6 +28,23 @@ export const TopNav = (props) => { }); }, [settings]); + useEffect(() => { + const handleKeyDown = (event) => { + if ((event.metaKey || event.ctrlKey) && event.key === "k") { + event.preventDefault(); + openSearch(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + const openSearch = () => { + searchDialog.handleOpen(); + }; + return ( { )} + openSearch()}> + + + + + { + const fields = useWatch({ control: formControl.control, name: "fields" }); + + useEffect(() => { + if (open) { + formControl.reset({ + fields: defaultValues.fields || [], + }); + } + }, [open, defaultValues, formControl]); + + const addField = () => { + formControl.reset({ + fields: [...fields, { name: "", value: "", type: "textField" }], + }); + }; + + const removeField = (index) => { + const newFields = fields.filter((_, i) => i !== index); + formControl.reset({ fields: newFields }); + }; + + const handleTypeChange = (index, newType) => { + const newFields = fields.map((field, i) => (i === index ? { ...field, type: newType } : field)); + formControl.reset({ fields: newFields }); + }; + + return ( + + Add/Edit Row + + + {Array.isArray(fields) && fields?.length > 0 && ( + <> + {fields.map((field, index) => ( + + + + + + handleTypeChange(index, e.target.value)} + fullWidth + sx={{ py: 1 }} + > + Text + Number + Boolean + + + + { + if (field.type === "switch") { + return { ml: 2 }; + } else if (field.type === "number") { + return { width: "100%" }; + } else { + return {}; + } + }} + /> + + + removeField(index)}> + + + + ))} + > + )} + }> + Add Property + + + + + + Cancel + + + Save + + + + ); +}; + +const Page = () => { + const pageTitle = "Table Maintenance"; + const apiUrl = "/api/ExecAzBobbyTables"; + const [tables, setTables] = useState([]); + const [selectedTable, setSelectedTable] = useState(null); + const [tableData, setTableData] = useState([]); + const addTableDialog = useDialog(); // Add dialog for adding table + const deleteTableDialog = useDialog(); // Add dialog for deleting table + const addEditRowDialog = useDialog(); // Add dialog for adding/editing row + const [defaultAddEditValues, setDefaultAddEditValues] = useState({}); + const [tableFilterParams, setTableFilterParams] = useState({ First: 1000 }); + const formControl = useForm({ + mode: "onChange", + }); + const [accordionExpanded, setAccordionExpanded] = useState(false); + + const addEditFormControl = useForm({ + mode: "onChange", + }); + + const filterFormControl = useForm({ + mode: "onChange", + defaultValues: { + First: 1000, + }, + }); + + const tableFilter = useWatch({ control: formControl.control, name: "tableFilter" }); + + const fetchTables = ApiPostCall({ + queryKey: "CippTables", + onResult: (result) => setTables(result), + }); + + const fetchTableData = ApiPostCall({ + queryKey: "CippTableData", + onResult: (result) => { + setTableData(result); + }, + }); + + const handleTableSelect = (tableName) => { + setSelectedTable(tableName); + fetchTableData.mutate({ + url: apiUrl, + data: { + FunctionName: "Get-AzDataTableEntity", + TableName: tableName, + Parameters: filterFormControl.getValues(), + }, + }); + }; + + const handleRefresh = () => { + if (selectedTable) { + fetchTableData.mutate({ + url: apiUrl, + data: { + FunctionName: "Get-AzDataTableEntity", + TableName: selectedTable, + Parameters: tableFilterParams, + }, + }); + } + }; + + const tableRowAction = ApiPostCall({ + queryKey: "CippTableRowAction", + onResult: handleRefresh, + }); + + const handleTableRefresh = () => { + fetchTables.mutate({ url: apiUrl, data: { FunctionName: "Get-AzDataTable", Parameters: {} } }); + }; + + const getSelectedProps = (data) => { + if (data?.Property && data?.Property.length > 0) { + var selectedProps = ["ETag", "PartitionKey", "RowKey"]; + data?.Property.map((prop) => { + if (selectedProps.indexOf(prop.value) === -1) { + selectedProps.push(prop.value); + } + }); + return selectedProps; + } else { + return []; + } + }; + + useEffect(() => { + handleTableRefresh(); + }, []); + + const actionItems = tables + .filter( + (table) => + tableFilter === "" || + tableFilter === undefined || + table.toLowerCase().includes(tableFilter.toLowerCase()) + ) + .map((table) => ({ + label: `${table}`, + customFunction: () => { + setTableData([]); + handleTableSelect(table); + }, + noConfirm: true, + })); + + const propertyItems = [ + { + label: "", + value: ( + + + + + + + ), + }, + ]; + + const getTableFields = () => { + if (tableData.length === 0) return []; + const sampleRow = tableData[0]; + return Object.keys(sampleRow) + .filter((key) => key !== "ETag" && key !== "Timestamp") + .map((key) => { + const value = sampleRow[key]; + let type = "textField"; + if (typeof value === "number") { + type = "number"; + } else if (typeof value === "boolean") { + type = "switch"; + } + return { + name: key, + label: key, + type: type, + required: false, + }; + }); + }; + + return ( + + + {pageTitle} + + + This page allows you to view and manage data in Azure Tables. This is advanced functionality + that should only be used when directed by CyberDrain support. + + + + a.label.localeCompare(b.label))} + isFetching={fetchTables.isPending} + cardSx={{ maxHeight: "calc(100vh - 170px)", overflow: "auto" }} + actionButton={ + + + + + + + + + + + + + + + + + } + /> + + + {selectedTable && ( + + + { + var properties = getSelectedProps(data); + setTableFilterParams({ ...data, Property: properties }); + handleRefresh(); + setAccordionExpanded(false); + })} + > + Apply Filters + + } + > + + + ({ + label: field?.label, + value: field?.name, + }))} + /> + + + + + + + + + { + setDefaultAddEditValues({ + fields: getTableFields().map((field) => ({ + name: field?.name, + value: "", + type: field?.type, + })), + }); + addEditRowDialog.handleOpen(); + }} // Open add/edit row dialog + startIcon={ + + + + } + > + Add Row + + + + + } + > + Delete Table + + + } + actions={[ + { + label: "Edit", + type: "POST", + icon: ( + + + + ), + customFunction: (row) => { + setDefaultAddEditValues({ + fields: Object.keys(row) + .filter((key) => key !== "ETag" && key !== "Timestamp") + .map((key) => { + const value = row[key]; + let type = "textField"; + if (typeof value === "number") { + type = "number"; + } else if (typeof value === "boolean") { + type = "switch"; + } + return { name: key, value: value, type: type }; + }), + }); + addEditRowDialog.handleOpen(); + }, + noConfirm: true, + hideBulk: true, + }, + { + label: "Delete", + type: "POST", + icon: ( + + + + ), + url: apiUrl, + customFunction: (row) => { + var entity = []; + if (Array.isArray(row)) { + entity = row.map((r) => ({ + RowKey: r.RowKey, + PartitionKey: r.PartitionKey, + ETag: r.ETag, + })); + } else { + entity = { + RowKey: row.RowKey, + PartitionKey: row.PartitionKey, + ETag: row.ETag, + }; + } + tableRowAction.mutate({ + url: apiUrl, + data: { + FunctionName: "Remove-AzDataTableEntity", + TableName: selectedTable, + Parameters: { + Entity: entity, + }, + }, + }); + }, + confirmText: + "Do you want to delete the selected row(s)? This action cannot be undone.", + multiPost: true, + }, + ]} + /> + + + )} + + + { + handleTableRefresh(); + }, + }} + /> + + + + Are you sure you want to delete this table? This is a destructive action that cannot + be undone. + + + ), + type: "POST", + data: { FunctionName: "Remove-AzDataTable", TableName: selectedTable, Parameters: {} }, + onSuccess: () => { + setSelectedTable(null); + setTableData([]); + handleTableRefresh(); + }, + }} + /> + { + const payload = data.fields.reduce((acc, field) => { + acc[field.name] = field.value; + return acc; + }, {}); + tableRowAction.mutate({ + url: apiUrl, + data: { + FunctionName: "Add-AzDataTableEntity", + TableName: selectedTable, + Parameters: { Entity: payload, Force: true }, + }, + onSuccess: handleRefresh, + }); + addEditRowDialog.handleClose(); + }} + defaultValues={defaultAddEditValues} + /> + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/integrations/configure.js b/src/pages/cipp/integrations/configure.js index eccc98cfd826..52d4766d9c09 100644 --- a/src/pages/cipp/integrations/configure.js +++ b/src/pages/cipp/integrations/configure.js @@ -44,6 +44,9 @@ const Page = () => { }; const handleIntegrationTest = () => { + if (testQuery.waiting) { + actionTestResults.refetch(); + } setTestQuery({ url: "/api/ExecExtensionTest", data: { diff --git a/src/pages/email/administration/mailbox-rules/index.js b/src/pages/email/administration/mailbox-rules/index.js index 84a5f8523d98..c80b749aad07 100644 --- a/src/pages/email/administration/mailbox-rules/index.js +++ b/src/pages/email/administration/mailbox-rules/index.js @@ -7,14 +7,13 @@ import { CippPropertyListCard } from "../../../../components/CippCards/CippPrope const Page = () => { const pageTitle = "Mailbox Rules"; - const simpleColumns = ["Name", "Priority", "Enabled", "UserPrincipalName", "From"]; const actions = [ { label: "Remove Mailbox Rule", type: "GET", icon: , url: "/api/ExecRemoveMailboxRule", - data: { ruleId: "Identity", userPrincipalName: "UserPrincipalName" }, + data: { ruleId: "Identity", userPrincipalName: "UserPrincipalName", ruleName: "Name" }, confirmText: "Are you sure you want to remove this mailbox rule?", multiPost: false, }, @@ -48,7 +47,8 @@ const Page = () => { diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index df5d899eb2e2..81726a1abd14 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -3,6 +3,18 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import Link from "next/link"; import { Button } from "@mui/material"; +import { + Archive, + MailOutline, + Person, + Room, + Visibility, + VisibilityOff, + PhonelinkLock, + Key, +} from "@mui/icons-material"; +import { TrashIcon, MagnifyingGlassIcon, PlayCircleIcon } from "@heroicons/react/24/outline"; + const Page = () => { const pageTitle = "Mailboxes"; @@ -10,71 +22,115 @@ const Page = () => { const actions = [ { label: "Edit permissions", - link: "/identity/administration/users/user/exchange?userId=[Id]", + link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", color: "info", + icon: , }, { label: "Research Compromised Account", - link: "/identity/administration/users/user/bec?userId=[UPN]", + link: "/identity/administration/users/user/bec?userId=[ExternalDirectoryObjectId]", color: "info", + icon: , }, { label: "Send MFA Push", type: "GET", url: "/api/ExecSendPush", data: { - UserEmail: "mail", + UserEmail: "UPN", }, confirmText: "Are you sure you want to send an MFA request?", + icon: , }, { label: "Convert to Shared Mailbox", type: "GET", + icon: , url: "/api/ExecConvertToSharedMailbox", data: { ID: "UPN", }, confirmText: "Are you sure you want to convert this mailbox to a shared mailbox?", + condition: (row) => row.recipientTypeDetails !== "SharedMailbox", + }, + { + label: "Convert to User Mailbox", + type: "GET", + url: "/api/ExecConvertToSharedMailbox", + icon: , + data: { + ID: "UPN", + ConvertToUser: true, + }, + confirmText: "Are you sure you want to convert this mailbox to a user mailbox?", + condition: (row) => row.recipientTypeDetails !== "UserMailbox", }, { label: "Convert to Room Mailbox", type: "GET", url: "/api/ExecConvertToRoomMailbox", + icon: , data: { ID: "UPN", }, confirmText: "Are you sure you want to convert this mailbox to a room mailbox?", + condition: (row) => row.recipientTypeDetails !== "RoomMailbox", }, { - label: "Hide from Global Address List", + //tested + label: "Enable Online Archive", type: "GET", + icon: , + url: "/api/ExecEnableArchive", + data: { ID: "UPN" }, + confirmText: "Are you sure you want to enable the online archive for this user?", + multiPost: false, + condition: (row) => row.ArchiveGuid === "00000000-0000-0000-0000-000000000000", + }, + { + label: "Hide from Global Address List", + type: "POST", url: "/api/ExecHideFromGAL", + icon: , data: { ID: "UPN", HidefromGAL: true, }, confirmText: "Are you sure you want to hide this mailbox from the global address list? This will not work if the user is AD Synced.", + condition: (row) => row.HiddenFromAddressListsEnabled === false, }, { label: "Unhide from Global Address List", - type: "GET", + type: "POST", url: "/api/ExecHideFromGAL", + icon: , data: { ID: "UPN", }, confirmText: "Are you sure you want to unhide this mailbox from the global address list? This will not work if the user is AD Synced.", + condition: (row) => row.HiddenFromAddressListsEnabled === true, }, { label: "Start Managed Folder Assistant", type: "GET", url: "/api/ExecStartManagedFolderAssistant", + icon: , data: { ID: "UPN", }, confirmText: "Are you sure you want to start the managed folder assistant for this user?", }, + { + label: "Delete Mailbox", + type: "GET", + icon: , // Added + url: "/api/RemoveMailbox", + data: { ID: "UPN" }, + confirmText: "Are you sure you want to delete this mailbox?", + multiPost: false, + }, ]; // Define off-canvas details @@ -83,13 +139,36 @@ const Page = () => { actions: actions, }; + const filterList = [ + { + filterName: "View User Mailboxes", + value: [{ id: "recipientTypeDetails", value: "UserMailbox" }], + type: "column", + }, + { + filterName: "View Shared Mailboxes", + value: [{ id: "recipientTypeDetails", value: "SharedMailbox" }], + type: "column", + }, + { + filterName: "View Room Mailboxes", + value: [{ id: "recipientTypeDetails", value: "RoomMailbox" }], + type: "column", + }, + { + filterName: "View Equipment Mailboxes", + value: [{ id: "recipientTypeDetails", value: "EquipmentMailbox" }], + type: "column", + }, + ]; + // Simplified columns for the table const simpleColumns = [ - "UPN", // User Principal Name "displayName", // Display Name + "recipientTypeDetails", // Recipient Type Details + "UPN", // User Principal Name "primarySmtpAddress", // Primary Email Address "recipientType", // Recipient Type - "recipientTypeDetails", // Recipient Type Details "AdditionalEmailAddresses", // Additional Email Addresses ]; @@ -100,6 +179,7 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + filters={filterList} cardButton={ <> diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js index eb24a4f73ccb..e784d8888a0e 100644 --- a/src/pages/email/administration/quarantine/index.js +++ b/src/pages/email/administration/quarantine/index.js @@ -133,6 +133,19 @@ const Page = () => { actions: actions, }; + const filterList = [ + { + filterName: "Not Released", + value: [{ id: "ReleaseStatus", value: "NOTRELEASED" }], + type: "column", + }, + { + filterName: "Released", + value: [{ id: "ReleaseStatus", value: "RELEASED" }], + type: "column", + }, + ]; + return ( <> { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + filters={filterList} /> setDialogOpen(false)} maxWidth="lg" fullWidth> diff --git a/src/pages/email/connectionfilter/list-templates/index.js b/src/pages/email/connectionfilter/list-templates/index.js index acd7b58f0ac7..4a104db53a18 100644 --- a/src/pages/email/connectionfilter/list-templates/index.js +++ b/src/pages/email/connectionfilter/list-templates/index.js @@ -6,19 +6,13 @@ const Page = () => { const pageTitle = "Connection filter Templates"; const actions = [ - { - label: "View Template", - icon: , // Placeholder for the view icon - color: "success", - offCanvas: true, - }, { label: "Delete Template", type: "POST", url: "/api/RemoveConnectionfilterTemplate", data: { ID: "GUID" }, confirmText: "Do you want to delete the template?", - icon: , // Placeholder for the delete icon + icon: , color: "danger", }, ]; diff --git a/src/pages/email/connectors/list-connector-templates/index.js b/src/pages/email/connectors/list-connector-templates/index.js index 02e97d3ca1f5..671c148f21e2 100644 --- a/src/pages/email/connectors/list-connector-templates/index.js +++ b/src/pages/email/connectors/list-connector-templates/index.js @@ -15,7 +15,7 @@ const Page = () => { ID: "GUID", }, confirmText: "Do you want to delete the template?", - icon: , // Placeholder for delete icon + icon: , color: "danger", }, ]; diff --git a/src/pages/email/reports/global-address-list/index.js b/src/pages/email/reports/global-address-list/index.js new file mode 100644 index 000000000000..5b0be1040072 --- /dev/null +++ b/src/pages/email/reports/global-address-list/index.js @@ -0,0 +1,81 @@ +ďťżimport { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; + +const Page = () => { + const actions = [ + { + label: "Unhide from Global Address List", + type: "POST", + url: "/api/ExecHideFromGAL", + data: { + HideFromGAL: false, + ID: "PrimarySmtpAddress", + }, + confirmText: "Are you sure you want to show this mailbox in the Global Address List?", + }, + { + label: "Hide from Global Address List", + type: "POST", + url: "/api/ExecHideFromGAL", + data: { + HideFromGAL: true, + ID: "PrimarySmtpAddress", + }, + confirmText: "Are you sure you want to hide this mailbox from the Global Address List?", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "HiddenFromAddressListsEnabled", + "ExternalDirectoryObjectId", + "DisplayName", + "PrimarySmtpAddress", + "RecipientType", + "RecipientTypeDetails", + "IsDirSynced", + "SKUAssigned", + "EmailAddresses", + ], + actions: actions, + }; + + const filters = [ + { + filterName: "Hidden from GAL", + value: [{ id: "HiddenFromAddressListsEnabled", value: "Yes" }], + type: "column", + }, + { + filterName: "Shown in GAL", + value: [{ id: "HiddenFromAddressListsEnabled", value: "No" }], + type: "column", + }, + { + filterName: "Cloud only mailboxes", + value: [{ id: "IsDirSynced", value: "No" }], + type: "column", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/spamfilter/list-templates/index.js b/src/pages/email/spamfilter/list-templates/index.js index 4c27ca4da7ed..faa7f9a905a0 100644 --- a/src/pages/email/spamfilter/list-templates/index.js +++ b/src/pages/email/spamfilter/list-templates/index.js @@ -6,12 +6,6 @@ const Page = () => { const pageTitle = "Spamfilter Templates"; const actions = [ - { - label: "View Template", - icon: , // Placeholder for the view icon - color: "success", - offCanvas: true, - }, { label: "Delete Template", type: "POST", diff --git a/src/pages/email/transport/list-rules/index.js b/src/pages/email/transport/list-rules/index.js index d7855ebe5bb1..7312f49794fc 100644 --- a/src/pages/email/transport/list-rules/index.js +++ b/src/pages/email/transport/list-rules/index.js @@ -1,6 +1,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Book, DoDisturb, Done } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; const Page = () => { @@ -11,50 +13,59 @@ const Page = () => { label: "Create template based on rule", type: "POST", url: "/api/AddTransportTemplate", - data: {}, // No extra data was specified for this action in the original file + postEntireRow: true, confirmText: "Are you sure you want to create a template based on this rule?", + icon: , }, { label: "Enable Rule", type: "POST", url: "/api/EditTransportRule", data: { - State: "Enable", - TenantFilter: "Tenant", + State: "!Enable", GUID: "Guid", }, confirmText: "Are you sure you want to enable this rule?", + icon: , }, { label: "Disable Rule", type: "POST", url: "/api/EditTransportRule", data: { - State: "Disable", - TenantFilter: "Tenant", + State: "!Disable", GUID: "Guid", }, confirmText: "Are you sure you want to disable this rule?", + icon: , }, { label: "Delete Rule", type: "POST", url: "/api/RemoveTransportRule", data: { - TenantFilter: "Tenant", GUID: "Guid", }, confirmText: "Are you sure you want to delete this rule?", color: "danger", + icon: , }, ]; const offCanvas = { - extendedInfoFields: ["CreatedBy", "LastModifiedBy", "Description"], + extendedInfoFields: [ + "Guid", + "CreatedBy", + "LastModifiedBy", + "WhenChanged", + "Name", + "Comments", + "Description", + ], actions: actions, }; - const simpleColumns = ["Name", "State", "Mode", "RuleErrorAction"]; + const simpleColumns = ["Name", "State", "Mode", "RuleErrorAction", "WhenChanged", "Comments"]; return ( { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + filters={[ + { + filterName: "Enabled Rules", + value: [{ id: "State", value: "Enabled" }], + type: "column", + }, + { + filterName: "Disabled Rules", + value: [{ id: "State", value: "Disabled" }], + type: "column", + }, + ]} cardButton={ <> diff --git a/src/pages/email/transport/list-templates/index.js b/src/pages/email/transport/list-templates/index.js index ff8afeb1fefe..1bfff7428f93 100644 --- a/src/pages/email/transport/list-templates/index.js +++ b/src/pages/email/transport/list-templates/index.js @@ -8,19 +8,13 @@ const Page = () => { const pageTitle = "Transport Rule Templates"; const actions = [ - { - label: "View Template", - icon: , // Placeholder icon for developer customization - color: "success", - offCanvas: true, - }, { label: "Delete Template", type: "POST", url: "/api/RemoveTransportRuleTemplate", data: { ID: "GUID" }, confirmText: "Do you want to delete the template?", - icon: , // Placeholder icon for developer customization + icon: , color: "danger", }, ]; diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index 75e54a4c88bb..e61d3930aedc 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -1,12 +1,35 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { EyeIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; const Page = () => { const pageTitle = "Available Endpoint Manager Templates"; const actions = [ + { + label: "Edit Template Name and Description", + type: "POST", + url: "/api/ExecEditTemplate", + fields: [ + { + type: "textField", + name: "displayName", + label: "Display Name", + }, + { + type: "textField", + name: "description", + label: "Description", + }, + ], + data: { GUID: "GUID", Type: "!IntuneTemplate" }, + confirmText: + "Enter the new name and description for the template. Warning: This will disconnect the template from a template library if applied.", + multiPost: false, + icon: , + color: "info", + }, { label: "Delete Template", type: "GET", diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx index ca226d78cc26..7bceb2e69857 100644 --- a/src/pages/endpoint/applications/list/add.jsx +++ b/src/pages/endpoint/applications/list/add.jsx @@ -86,6 +86,7 @@ const ApplicationDeploymentForm = () => { const formattedData = { ...data }; formattedData.selectedTenants = selectedTenants.map((tenant) => ({ defaultDomainName: tenant.value, + customerId: tenant.addedFields.customerId, })); return formattedData; }} @@ -174,11 +175,12 @@ const ApplicationDeploymentForm = () => { /> {selectedTenants?.map((tenant, index) => ( - + + {console.log(tenant)} @@ -194,11 +196,11 @@ const ApplicationDeploymentForm = () => { compareValue="syncro" > {selectedTenants?.map((tenant, index) => ( - + @@ -215,11 +217,11 @@ const ApplicationDeploymentForm = () => { compareValue="immy" > {selectedTenants?.map((tenant, index) => ( - + @@ -244,11 +246,11 @@ const ApplicationDeploymentForm = () => { /> {selectedTenants?.map((tenant, index) => ( - + @@ -273,22 +275,22 @@ const ApplicationDeploymentForm = () => { /> {selectedTenants?.map((tenant, index) => ( - + ))} {selectedTenants?.map((tenant, index) => ( - + @@ -304,11 +306,11 @@ const ApplicationDeploymentForm = () => { compareValue="cwcommand" > {selectedTenants?.map((tenant, index) => ( - + diff --git a/src/pages/endpoint/applications/queue/index.js b/src/pages/endpoint/applications/queue/index.js index 7ed456ad97d5..0f30341cefcb 100644 --- a/src/pages/endpoint/applications/queue/index.js +++ b/src/pages/endpoint/applications/queue/index.js @@ -5,22 +5,16 @@ import { Button } from "@mui/material"; import Link from "next/link"; import { ApiPostCall } from "../../../../api/ApiCall"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; +import { TrashIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Queued Applications"; const actions = [ - { - label: "Deploy now", - type: "POST", - url: "/api/ExecAppUpload", - confirmText: - "Deploy all queued applications to tenants?\n\nNote: This job runs automatically every 12 hours.", - multiPost: false, - }, { label: "Delete Application", type: "POST", + icon: , url: "/api/RemoveQueuedApp", data: { ID: "id" }, confirmText: "Do you want to delete the queued application?", diff --git a/src/pages/endpoint/autopilot/add-profile/index.js b/src/pages/endpoint/autopilot/add-profile/index.js deleted file mode 100644 index 83f5fdb1a829..000000000000 --- a/src/pages/endpoint/autopilot/add-profile/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Add Profile"; - - return ( - - {pageTitle} - This is a placeholder page for the add profile section. - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/endpoint/reports/analyticsdevicescore/index.js b/src/pages/endpoint/reports/analyticsdevicescore/index.js index 563f2168b1ff..16702d203621 100644 --- a/src/pages/endpoint/reports/analyticsdevicescore/index.js +++ b/src/pages/endpoint/reports/analyticsdevicescore/index.js @@ -1,23 +1,23 @@ import { EyeIcon } from "@heroicons/react/24/outline"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; const Page = () => { const pageTitle = "Analytics Device Score Report"; + const tenantFilter = useSettings().currentTenant; // Actions from the source file const actions = [ - /* TODO: Add direct link to InTune Device { - label: "View Device", - type: "LINK", - link: "https://intune.microsoft.com/[tenant]/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]", - linkParams: { - tenant: "TenantFilter", - id: "id", - }, + 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 diff --git a/src/pages/endpoint/reports/devices/index.js b/src/pages/endpoint/reports/devices/index.js index 4bce06237a4c..4ce9ebcbc46a 100644 --- a/src/pages/endpoint/reports/devices/index.js +++ b/src/pages/endpoint/reports/devices/index.js @@ -1,8 +1,11 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; +import { EyeIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Devices"; + const tenantFilter = useSettings().currentTenant; const actions = [ { @@ -96,6 +99,15 @@ const Page = () => { confirmText: "Are you sure you want to update the Windows Defender signatures for this device?", }, + { + 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, + }, ]; const offCanvas = { diff --git a/src/pages/identity/administration/devices/index.js b/src/pages/identity/administration/devices/index.js index 30cfcc151253..456329c33755 100644 --- a/src/pages/identity/administration/devices/index.js +++ b/src/pages/identity/administration/devices/index.js @@ -1,60 +1,67 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import { useSettings } from "/src/hooks/use-settings"; +import { EyeIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Devices"; + const tenantFilter = useSettings().currentTenant; + const actions = [ - // these are currently GET requests that should be converted to POST requests. + { + label: "View in Entra", + link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_Devices/DeviceDetailsMenuBlade/~/Properties/objectId/[id]/deviceId/`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, { label: "Enable Device", - type: "GET", + type: "POST", url: "/api/ExecDeviceDelete", data: { ID: "id", - Action: "!Enable", + action: "!Enable", }, confirmText: "Are you sure you want to enable this device?", multiPost: false, }, { label: "Disable Device", - type: "GET", + type: "POST", url: "/api/ExecDeviceDelete", data: { ID: "id", - Action: "!Disable", + action: "!Disable", }, confirmText: "Are you sure you want to disable this device?", multiPost: false, }, { - label: "Retrieve Bitlocker Keys", + label: "Retrieve BitLocker Keys", type: "GET", url: "/api/ExecGetRecoveryKey", data: { GUID: "id", }, - confirmText: "Are you sure you want to retrieve the Bitlocker keys?", + confirmText: "Are you sure you want to retrieve the BitLocker keys?", multiPost: false, }, { label: "Delete Device", - type: "GET", + type: "POST", url: "/api/ExecDeviceDelete", data: { ID: "id", - Action: "!Delete", + action: "!Delete", }, confirmText: "Are you sure you want to delete this device?", multiPost: false, }, ]; - const offCanvas = { - extendedInfoFields: ["createdDateTime", "displayName", "id"], - actions: actions, - }; - return ( { }} apiDataKey="Results" actions={actions} - offCanvas={offCanvas} simpleColumns={[ "displayName", "accountEnabled", @@ -77,6 +83,7 @@ const Page = () => { "operatingSystem", "operatingSystemVersion", "profileType", + "approximateLastSignInDateTime", ]} /> ); diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index dd174b573769..1fb57be03d1e 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -2,7 +2,8 @@ import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import Link from "next/link"; -import { EyeIcon } from "@heroicons/react/24/outline"; +import { EyeIcon, LockClosedIcon, LockOpenIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { LockOpen, Visibility, VisibilityOff } from "@mui/icons-material"; const Page = () => { const pageTitle = "Groups"; @@ -12,13 +13,14 @@ const Page = () => { label: "Edit Group", link: "/identity/administration/groups/edit?groupId=[id]", multiPost: false, - icon: , + icon: , color: "success", }, { label: "Hide from Global Address List", type: "GET", url: "/api/ExecGroupsHideFromGAL", + icon: , data: { TenantFilter: "TenantFilter", ID: "mail", @@ -33,6 +35,7 @@ const Page = () => { label: "Unhide from Global Address List", type: "GET", url: "/api/ExecGroupsHideFromGAL", + icon: , data: { TenantFilter: "TenantFilter", ID: "mail", @@ -46,6 +49,7 @@ const Page = () => { label: "Only allow messages from people inside the organisation", type: "GET", url: "/api/ExecGroupsDeliveryManagement", + icon: , data: { TenantFilter: "TenantFilter", ID: "mail", @@ -59,6 +63,7 @@ const Page = () => { { label: "Allow messages from people inside and outside the organisation", type: "GET", + icon: , url: "/api/ExecGroupsDeliveryManagement", data: { TenantFilter: "TenantFilter", @@ -73,6 +78,7 @@ const Page = () => { label: "Delete Group", type: "GET", url: "/api/ExecGroupsDelete", + icon: , data: { ID: "id", GroupType: "calculatedGroupType", @@ -105,7 +111,17 @@ const Page = () => { > } - apiUrl="/api/ListGroups" + apiUrl="/api/ListGraphRequest" + apiData={{ + Endpoint: "groups", + $select: + "id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName,assignedLicenses", + $count: true, + $orderby: "displayName", + $top: 999, + manualPagination: true, + }} + apiDataKey="Results" actions={actions} offCanvas={offCanvas} simpleColumns={[ @@ -115,6 +131,7 @@ const Page = () => { "mailEnabled", "mailNickname", "calculatedGroupType", + "assignedLicenses", "visibility", "onPremisesSamAccountName", "membershipRule", diff --git a/src/pages/identity/administration/risky-users/index.js b/src/pages/identity/administration/risky-users/index.js index c235ed4e2b2f..759117480d8b 100644 --- a/src/pages/identity/administration/risky-users/index.js +++ b/src/pages/identity/administration/risky-users/index.js @@ -1,11 +1,11 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Clear } from "@mui/icons-material"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Risky Users"; - // Actions from the source file const actions = [ { label: "Dismiss Risk", @@ -16,22 +16,56 @@ const Page = () => { confirmText: "Are you sure you want to dismiss the risk for this user?", multiPost: false, }, + { + label: "Research Compromised Account", + type: "GET", + icon: , + link: "/identity/administration/users/user/bec?userId=[id]", + confirmText: "Are you sure you want to research this compromised account?", + multiPost: false, + }, ]; - // OffCanvas details based on the source file const offCanvas = { extendedInfoFields: [ - "id", // User ID - "userDisplayName", // Display Name - "userPrincipalName", // User Principal - "riskLastUpdatedDateTime", // Risk Last Updated - "riskLevel", // Risk Level - "riskState", // Risk State - "riskDetail", // Risk Detail + "id", + "userDisplayName", + "userPrincipalName", + "riskLastUpdatedDateTime", + "riskLevel", + "riskState", + "riskDetail", ], actions: actions, }; + const simpleColumns = [ + "userDisplayName", + "userPrincipalName", + "riskLevel", + "riskState", + "riskDetail", + "riskLastUpdatedDateTime", + ]; + + const filterList = [ + { + filterName: "Users at Risk", + value: [{ id: "riskState", value: "atRisk" }], + type: "column", + }, + { + filterName: "Dismissed Users", + value: [{ id: "riskState", value: "dismissed" }], + type: "column", + }, + { + filterName: "Remediated Users", + value: [{ id: "riskState", value: "remediated" }], + type: "column", + }, + ]; + return ( { apiData={{ Endpoint: "identityProtection/riskyUsers", manualPagination: true, - $select: - "id,userDisplayName,userPrincipalName,riskLevel,riskState,riskDetail,riskLastUpdatedDateTime", $count: true, $orderby: "riskLastUpdatedDateTime desc", $top: 500, @@ -48,14 +80,8 @@ const Page = () => { apiDataKey="Results" actions={actions} offCanvas={offCanvas} - simpleColumns={[ - "userDisplayName", - "userPrincipalName", - "riskLevel", - "riskState", - "riskDetail", - "riskLastUpdatedDateTime", - ]} + simpleColumns={simpleColumns} + filters={filterList} /> ); }; diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index 0c3c6c773375..c03a62bb56cd 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -1,312 +1,14 @@ -import { EyeIcon, MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { - Archive, - Block, - Clear, - CloudDone, - Edit, - Email, - ForwardToInbox, - GroupAdd, - LockOpen, - LockPerson, - LockReset, - MeetingRoom, - NoMeetingRoom, - Password, - PersonOff, - PhonelinkLock, - PhonelinkSetup, - Shortcut, -} from "@mui/icons-material"; import { Button } from "@mui/material"; import Link from "next/link"; import { useSettings } from "/src/hooks/use-settings.js"; +import { CippUserActions } from "/src/components/CippComponents/CippUserActions.jsx"; const Page = () => { const pageTitle = "Users"; const tenant = useSettings().currentTenant; - const actions = [ - { - //tested - label: "View User", - link: "/identity/administration/users/user?userId=[id]", - multiPost: false, - icon: , - color: "success", - }, - { - //tested - label: "Edit User", - link: "/identity/administration/users/user/edit?userId=[id]", - icon: , - color: "success", - target: "_self", - }, - { - //tested - label: "Research Compromised Account", - type: "GET", - icon: , - link: "/identity/administration/users/user/bec?userId=[id]", - confirmText: "Are you sure you want to research this compromised account?", - multiPost: false, - }, - { - //tested - - label: "Create Temporary Access Password", - type: "GET", - icon: , - url: "/api/ExecCreateTAP", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to create a Temporary Access Password?", - multiPost: false, - }, - { - //tested - label: "Re-require MFA registration", - type: "GET", - icon: , - url: "/api/ExecResetMFA", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to reset MFA for this user?", - multiPost: false, - }, - { - //tested - label: "Send MFA Push", - type: "POST", - icon: , - url: "/api/ExecSendPush", - data: { UserEmail: "userPrincipalName" }, - confirmText: "Are you sure you want to send an MFA request?", - multiPost: false, - }, - { - //tested - label: "Set Per-User MFA", - type: "POST", - icon: , - url: "/api/ExecPerUserMFA", - data: { userId: "userPrincipalName" }, - fields: [ - { - type: "autoComplete", - name: "State", - label: "State", - options: [ - { label: "Enforced", value: "Enforced" }, - { label: "Enabled", value: "Enabled" }, - { label: "Disabled", value: "Disabled" }, - ], - multiple: false, - }, - ], - confirmText: "Are you sure you want to set per-user MFA for these users?", - multiPost: false, - }, - { - //tested - label: "Convert to Shared Mailbox", - type: "GET", - icon: , - url: "/api/ExecConvertToSharedMailbox", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to convert this user to a shared mailbox?", - multiPost: false, - }, - { - //tested - label: "Enable Online Archive", - type: "GET", - icon: , - url: "/api/ExecEnableArchive", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to enable the online archive for this user?", - multiPost: false, - }, - { - //tested - label: "Set Out of Office", - type: "POST", - icon: , - url: "/api/ExecSetOoO", - data: { - userId: "userPrincipalName", - AutoReplyState: { value: "Enabled" }, - tenantFilter: "Tenant", - }, - fields: [{ type: "richText", name: "input", label: "Out of Office Message" }], - confirmText: "Are you sure you want to set the out of office?", - multiPost: false, - }, - - { - label: "Disable Out of Office", - type: "POST", - icon: , - url: "/api/ExecSetOoO", - data: { user: "userPrincipalName", AutoReplyState: "Disabled" }, - confirmText: "Are you sure you want to disable the out of office?", - multiPost: false, - }, - { - label: "Add to Group", - type: "POST", - icon: , - url: "/api/EditGroup", - data: { addMember: "userPrincipalName" }, - fields: [ - { - type: "autoComplete", - name: "groupId", - label: "Select a group to add the user to", - multiple: false, - creatable: false, - api: { - url: "/api/ListGroups", - labelField: "displayName", - valueField: "id", - addedField: { - groupType: "calculatedGroupType", - groupName: "displayName", - }, - queryKey: `groups-${tenant}`, - }, - }, - ], - confirmText: "Are you sure you want to add the user to this group?", - }, - { - label: "Disable Email Forwarding", - type: "POST", - url: "/api/ExecEmailForward", - icon: , - data: { - username: "userPrincipalName", - userid: "userPrincipalName", - ForwardOption: "!disabled", - }, - confirmText: "Are you sure you want to disable forwarding of this user's emails?", - multiPost: false, - }, - { - label: "Pre-provision OneDrive", - type: "POST", - icon: , - url: "/api/ExecOneDriveProvision", - data: { UserPrincipalName: "userPrincipalName" }, - confirmText: "Are you sure you want to pre-provision OneDrive for this user?", - multiPost: false, - }, - { - label: "Add OneDrive Shortcut", - type: "POST", - icon: , - url: "/api/ExecOneDriveShortCut", - data: { - username: "userPrincipalName", - userid: "id", - }, - fields: [ - { - type: "autoComplete", - name: "siteUrl", - label: "Select a Site", - multiple: false, - creatable: false, - api: { - url: "/api/ListSites", - data: { type: "SharePointSiteUsage", URLOnly: true }, - labelField: "webUrl", - valueField: "webUrl", - queryKey: `sharepointSites-${tenant}`, - }, - }, - ], - confirmText: "Select a SharePoint site to create a shortcut for:", - multiPost: false, - }, - { - label: "Block Sign In", - type: "GET", - icon: , - url: "/api/ExecDisableUser", - data: { ID: "id" }, - confirmText: "Are you sure you want to block the sign-in for this user?", - multiPost: false, - }, - { - label: "Unblock Sign In", - type: "GET", - icon: , - url: "/api/ExecDisableUser", - data: { ID: "id", Enable: true }, - confirmText: "Are you sure you want to unblock sign-in for this user?", - multiPost: false, - }, - { - label: "Reset Password (Must Change)", - type: "GET", - icon: , - url: "/api/ExecResetPass", - data: { - MustChange: true, - ID: "userPrincipalName", - displayName: "displayName", - }, - confirmText: - "Are you sure you want to reset the password for this user? The user must change their password at next logon.", - multiPost: false, - }, - { - label: "Reset Password", - type: "GET", - icon: , - url: "/api/ExecResetPass", - data: { - MustChange: false, - ID: "userPrincipalName", - displayName: "displayName", - }, - confirmText: "Are you sure you want to reset the password for this user?", - multiPost: false, - }, - { - label: "Clear Immutable ID", - type: "GET", - icon: , - url: "/api/ExecClrImmId", - data: { - ID: "id", - }, - confirmText: "Are you sure you want to clear the Immutable ID for this user?", - multiPost: false, - }, - { - label: "Revoke all user sessions", - type: "GET", - icon: , - url: "/api/ExecRevokeSessions", - data: { ID: "id", Username: "userPrincipalName" }, - confirmText: "Are you sure you want to revoke all sessions for this user?", - multiPost: false, - }, - { - label: "Delete User", - type: "GET", - icon: , - url: "/api/RemoveUser", - data: { ID: "id" }, - confirmText: "Are you sure you want to delete this user?", - multiPost: false, - }, - ]; - const offCanvas = { extendedInfoFields: [ "createdDateTime", // Created Date (UTC) @@ -324,7 +26,7 @@ const Page = () => { "id", // Unique ID "otherMails", // Alternate Email Addresses ], - actions: actions, + actions: CippUserActions(), }; return ( @@ -354,7 +56,7 @@ const Page = () => { $top: 999, }} apiDataKey="Results" - actions={actions} + actions={CippUserActions()} offCanvas={offCanvas} simpleColumns={[ "accountEnabled", diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index bb42a2a87f03..6bda3f8534c2 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -25,12 +25,8 @@ const Page = () => { const { userId } = router.query; const tenant = userSettingsDefaults.currentTenant; - const currentSettings = userSettingsDefaults.currentSettings; // Assuming currentSettings is part of useSettings + const [formParams, setFormParams] = useState(false); - // State for form parameters - const [formParams, setFormParams] = useState(null); - - // Fetch user details for the header const userRequest = ApiGetCall({ url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`, queryKey: `ListUsers-${userId}`, @@ -101,7 +97,6 @@ const Page = () => { Test policies } - cardLabelBox={currentSettings?.ForwardAndDeliver ? : "-"} // Optional: Display an icon or placeholder > {/* Form Starts Here */} @@ -234,7 +229,7 @@ const Page = () => { title={"CA Test Results"} simple={true} simpleColumns={["displayName", "state", "policyApplies", "reasons"]} - data={postRequest.data?.data?.Results?.value} + data={postRequest.data?.data?.Results?.value || []} /> diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index a9d325f8f35a..29082e3b6fb8 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -18,6 +18,10 @@ import CippExchangeSettingsForm from "../../../../../components/CippFormPages/Ci import { useForm } from "react-hook-form"; import { Alert, Button, Collapse, CircularProgress, Typography } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../../../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; const Page = () => { const userSettingsDefaults = useSettings(); @@ -55,6 +59,12 @@ const Page = () => { waiting: waiting, }); + const mailboxRulesRequest = ApiGetCall({ + url: `/api/ListUserMailboxRules?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `MailboxRules-${userId}`, + waiting: waiting, + }); + useEffect(() => { if (oooRequest.isSuccess) { formControl.setValue("ooo.ExternalMessage", oooRequest.data?.ExternalMessage); @@ -156,16 +166,87 @@ const Page = () => { })) || [], }, ]; + + const mailboxRuleActions = [ + { + label: "Remove Mailbox Rule", + type: "GET", + icon: , + url: "/api/ExecRemoveMailboxRule", + data: { + ruleId: "Identity", + ruleName: "Name", + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + }, + confirmText: "Are you sure you want to remove this mailbox rule?", + multiPost: false, + relatedQueryKeys: `MailboxRules-${userId}`, + }, + ]; + + const mailboxRulesCard = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: mailboxRulesRequest.isFetching ? ( + + ) : mailboxRulesRequest.data?.length !== 0 ? ( + + ) : ( + + ), + }, + text: "Current Mailbox Rules", + subtext: mailboxRulesRequest.data?.length + ? "Mailbox rules are configured for this user" + : "No mailbox rules configured for this user", + statusColor: "green.main", + table: { + title: "Mailbox Rules", + hideTitle: true, + data: mailboxRulesRequest.data || [], + refreshFunction: () => mailboxRulesRequest.refetch(), + isFetching: mailboxRulesRequest.isFetching, + simpleColumns: ["Enabled", "Name", "Description", "Priority"], + actions: mailboxRuleActions, + offCanvas: { + 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 ( - {userRequest.isLoading && } - {userRequest.isSuccess && ( + {graphUserRequest.isLoading && } + {graphUserRequest.isSuccess && ( { )} - {!userRequest?.data?.[0]?.Mailbox?.[0]?.error && ( + {!userRequest?.data?.[0]?.Mailbox?.[0]?.error?.includes( + "Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException" + ) && ( <> @@ -215,6 +298,11 @@ const Page = () => { items={calCard} isCollapsible={calPermissions.data?.length !== 0} /> + import("/src/components/CippComponents/CippMap"), { ssr: false }); + +import { Button, Dialog, DialogTitle, DialogContent, IconButton } from "@mui/material"; +import { Close } from "@mui/icons-material"; +import { CippPropertyList } from "../../../../../components/CippComponents/CippPropertyList"; + +const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { + return ( + + + Sign-In Logs + + + + + + + + + ); +}; const Page = () => { - const popover = usePopover(); - const createDialog = useDialog(); const userSettingsDefaults = useSettings(); const router = useRouter(); const { userId } = router.query; const [waiting, setWaiting] = useState(false); + const [signInLogsDialogOpen, setSignInLogsDialogOpen] = useState(false); + useEffect(() => { if (userId) { setWaiting(true); } }, [userId]); + const userRequest = ApiGetCall({ url: `/api/ListUsers?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, queryKey: `ListUsers-${userId}`, waiting: waiting, }); + const userMemberOf = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `/users/${userId}/memberOf`, + tenantFilter: userSettingsDefaults.currentTenant, + $top: 99, + }, + queryKey: `UserMemberOf-${userId}`, + }); + const MFARequest = ApiGetCall({ url: "/api/ListGraphRequest", data: { @@ -120,6 +158,20 @@ const Page = () => { subtext: `Logged into application ${signInData.resourceDisplayName || "Unknown Application"}`, statusColor: signInData.status.errorCode === 0 ? "success.main" : "error.main", statusText: signInData.status.errorCode === 0 ? "Success" : "Failed", + actionButton: ( + setSignInLogsDialogOpen(true)} + startIcon={ + + + + } + > + More Sign-In Logs + + ), propertyItems: [ { label: "Client App Used", @@ -139,6 +191,39 @@ const Page = () => { value: signInData.status?.additionalDetails || "N/A", }, ], + children: ( + <> + {signInData?.location && ( + <> + Location + + + + + + + + + > + )} + > + ), }; // Prepare the conditional access policies items @@ -356,288 +441,61 @@ const Page = () => { }, ]; } - const actions = [ - { - //tested - label: "View User", - link: "/identity/administration/users/user?userId=[id]", - multiPost: false, - icon: , - color: "success", - }, - { - //tested - label: "Edit User", - link: "/identity/administration/users/user/edit?userId=[id]", - icon: , - color: "success", - target: "_self", - }, - { - //tested - label: "Research Compromised Account", - type: "GET", - icon: , - link: "/identity/administration/users/user/bec?userId=[id]", - confirmText: "Are you sure you want to research this compromised account?", - multiPost: false, - }, - { - //tested - label: "Create Temporary Access Password", - type: "GET", - icon: , - url: "/api/ExecCreateTAP", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to create a Temporary Access Password?", - multiPost: false, - }, - { - //tested - label: "Rerequire MFA registration", - type: "GET", - icon: , - url: "/api/ExecResetMFA", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to reset MFA for this user?", - multiPost: false, - }, - { - //tested - label: "Send MFA Push", - type: "POST", - icon: , - url: "/api/ExecSendPush", - data: { UserEmail: "userPrincipalName" }, - confirmText: "Are you sure you want to send an MFA request?", - multiPost: false, - }, - { - //tested - label: "Set Per-User MFA", - type: "POST", - icon: , - url: "/api/ExecPerUserMFA", - data: { userId: "userPrincipalName" }, - fields: [ + const groupMembershipItems = userMemberOf.isSuccess + ? [ { - type: "autoComplete", - name: "State", - label: "State", - options: [ - { label: "Enforced", value: "Enforced" }, - { label: "Enabled", value: "Enabled" }, - { label: "Disabled", value: "Disabled" }, - ], - multiple: false, + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Groups", + subtext: "List of groups the user is a member of", + table: { + title: "Group Memberships", + hideTitle: true, + actions: [ + { + icon: , + label: "Edit Group", + link: "/identity/administration/groups/edit?groupId=[id]", + }, + ], + data: userMemberOf?.data?.Results.filter( + (item) => item?.["@odata.type"] === "#microsoft.graph.group" + ), + simpleColumns: ["displayName", "groupTypes", "securityEnabled", "mailEnabled"], + }, }, - ], - confirmText: "Are you sure you want to set per-user MFA for these users?", - multiPost: false, - }, - { - //tested - label: "Convert to Shared Mailbox", - type: "GET", - icon: , - url: "/api/ExecConvertToSharedMailbox", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to convert this user to a shared mailbox?", - multiPost: false, - }, - { - //tested - label: "Enable Online Archive", - type: "GET", - icon: , - url: "/api/ExecEnableArchive", - data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to enable the online archive for this user?", - multiPost: false, - }, - { - //tested - label: "Set Out of Office", - type: "POST", - icon: , - url: "/api/ExecSetOoO", - data: { - userId: "userPrincipalName", - AutoReplyState: { value: "Enabled" }, - }, - fields: [{ type: "richText", name: "input", label: "Out of Office Message" }], - confirmText: "Are you sure you want to set the out of office?", - multiPost: false, - }, + ] + : []; - { - label: "Disable Out of Office", - type: "POST", - icon: , - url: "/api/ExecSetOoO", - data: { user: "userPrincipalName", AutoReplyState: "Disabled" }, - confirmText: "Are you sure you want to disable the out of office?", - multiPost: false, - }, - { - label: "Add to Group", - type: "POST", - icon: , - url: "/api/EditGroup", - data: { addMember: "userPrincipalName" }, - fields: [ + const roleMembershipItems = userMemberOf.isSuccess + ? [ { - type: "autoComplete", - name: "groupId", - label: "Select a group to add the user to", - multiple: false, - creatable: false, - api: { - url: "/api/ListGroups", - labelField: "displayName", - valueField: "id", - addedField: { - groupType: "calculatedGroupType", - groupName: "displayName", - }, - queryKey: `groups-${userSettingsDefaults.currentTenant}}`, + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , }, - }, - ], - confirmText: "Are you sure you want to add the user to this group?", - }, - { - label: "Disable Email Forwarding", - type: "POST", - url: "/api/ExecEmailForward", - icon: , - data: { - username: "userPrincipalName", - userid: "userPrincipalName", - ForwardOption: "!disabled", - }, - confirmText: "Are you sure you want to disable forwarding of this user's emails?", - multiPost: false, - }, - { - label: "Pre-provision OneDrive", - type: "POST", - icon: , - url: "/api/ExecOneDriveProvision", - data: { UserPrincipalName: "userPrincipalName" }, - confirmText: "Are you sure you want to pre-provision OneDrive for this user?", - multiPost: false, - }, - { - label: "Add OneDrive Shortcut", - type: "POST", - icon: , - url: "/api/ExecOneDriveShortCut", - data: { - username: "userPrincipalName", - userid: "id", - }, - fields: [ - { - type: "autoComplete", - name: "siteUrl", - label: "Select a Site", - multiple: false, - creatable: false, - api: { - url: "/api/ListSites", - data: { type: "SharePointSiteUsage", URLOnly: true }, - labelField: "webUrl", - valueField: "webUrl", - queryKey: `sharepointSites-${userSettingsDefaults.currentTenant}}`, + text: "Admin Roles", + subtext: "List of roles the user is a member of", + table: { + title: "Admin Roles", + hideTitle: true, + data: userMemberOf?.data?.Results.filter( + (item) => item?.["@odata.type"] === "#microsoft.graph.directoryRole" + ), + simpleColumns: ["displayName", "description"], }, }, - ], - confirmText: "Select a SharePoint site to create a shortcut for:", - multiPost: false, - }, - { - label: "Block Sign In", - type: "GET", - icon: , - url: "/api/ExecDisableUser", - data: { ID: "id" }, - confirmText: "Are you sure you want to block the sign-in for this user?", - multiPost: false, - }, - { - label: "Unblock Sign In", - type: "GET", - icon: , - url: "/api/ExecDisableUser", - data: { ID: "id", Enable: true }, - confirmText: "Are you sure you want to unblock sign-in for this user?", - multiPost: false, - }, - { - label: "Reset Password (Must Change)", - type: "GET", - icon: , - url: "/api/ExecResetPass", - data: { - MustChange: true, - ID: "userPrincipalName", - displayName: "displayName", - }, - confirmText: - "Are you sure you want to reset the password for this user? The user must change their password at next logon.", - multiPost: false, - }, - { - label: "Reset Password", - type: "GET", - icon: , - url: "/api/ExecResetPass", - data: { - MustChange: false, - ID: "userPrincipalName", - displayName: "displayName", - }, - confirmText: "Are you sure you want to reset the password for this user?", - multiPost: false, - }, - { - label: "Clear Immutable ID", - type: "GET", - icon: , - url: "/api/ExecClrImmId", - data: { - ID: "id", - }, - confirmText: "Are you sure you want to clear the Immutable ID for this user?", - multiPost: false, - }, - { - label: "Revoke all user sessions", - type: "GET", - icon: , - url: "/api/ExecRevokeSessions", - data: { ID: "id", Username: "userPrincipalName" }, - confirmText: "Are you sure you want to revoke all sessions for this user?", - multiPost: false, - }, - { - label: "Delete User", - type: "GET", - icon: , - url: "/api/RemoveUser", - data: { ID: "id" }, - confirmText: "Are you sure you want to delete this user?", - multiPost: false, - }, - ]; + ] + : []; return ( { items={mfaDevicesItems} isCollapsible={mfaDevicesItems.length > 0 ? true : false} /> + Memberships + 0 ? true : false} + /> + 0 ? true : false} + /> )} + setSignInLogsDialogOpen(false)} + userId={userId} + tenantFilter={userSettingsDefaults.currentTenant} + /> ); }; diff --git a/src/pages/identity/reports/risk-detections/index.js b/src/pages/identity/reports/risk-detections/index.js index f6e634d0034c..f6fe5e6b07da 100644 --- a/src/pages/identity/reports/risk-detections/index.js +++ b/src/pages/identity/reports/risk-detections/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Risk Detection Report"; @@ -8,8 +9,11 @@ const Page = () => { const actions = [ { label: "Research Compromised Account", - link: "/identity/administration/users/user/bec?userId=[userId]&tenantFilter=[Tenant]", - color: "info", + type: "GET", + icon: , + link: "/identity/administration/users/user/bec?userId=[userId]", + confirmText: "Are you sure you want to research this compromised account?", + multiPost: false, }, ]; @@ -47,18 +51,28 @@ const Page = () => { "activity", ]; - // Note to Developer: Add necessary filter logic here - // Filters previously defined: - /* - filterlist: [ - { filterName: 'State: atRisk', filter: 'Complex: riskState eq atRisk' }, - { filterName: 'State: confirmedCompromised', filter: 'Complex: riskState eq confirmedCompromised' }, - { filterName: 'State: confirmedSafe', filter: 'Complex: riskState eq confirmedSafe' }, - { filterName: 'State: dismissed', filter: 'Complex: riskState eq dismissed' }, - { filterName: 'State: remediated', filter: 'Complex: riskState eq remediated' }, - { filterName: 'State: unknownFutureValue', filter: 'Complex: riskState eq unknownFutureValue' }, - ] - */ + const filterList = [ + { + filterName: "Users at Risk", + value: [{ id: "riskState", value: "atRisk" }], + type: "column", + }, + { + filterName: "Confirmed Compromised", + value: [{ id: "riskState", value: "confirmedCompromised" }], + type: "column", + }, + { + filterName: "Confirmed Safe", + value: [{ id: "riskState", value: "confirmedSafe" }], + type: "column", + }, + { + filterName: "Remediated", + value: [{ id: "riskState", value: "remediated" }], + type: "column", + }, + ]; return ( { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + filters={filterList} /> ); }; diff --git a/src/pages/identity/reports/signin-report/index.js b/src/pages/identity/reports/signin-report/index.js index 4f89e7a4b0b4..6304b8fa0da6 100644 --- a/src/pages/identity/reports/signin-report/index.js +++ b/src/pages/identity/reports/signin-report/index.js @@ -1,5 +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 CippButtonCard from "/src/components/CippCards/CippButtonCard"; const Page = () => { const pageTitle = "Sign Ins Report"; @@ -16,14 +19,93 @@ const Page = () => { "locationcipp", ]; + const [filterValues, setFilterValues] = useState({ + Days: 7, + filter: "", + failedLogonsOnly: false, + FailureThreshold: 0, + }); + + const [appliedFilters, setAppliedFilters] = useState(filterValues); + + const handleFilterChange = (event) => { + const { name, value, type, checked } = event.target; + setFilterValues({ + ...filterValues, + [name]: type === "checkbox" ? checked : value, + }); + }; + + const handleFilterSubmit = () => { + setAppliedFilters(filterValues); + }; + + const tableFilter = ( + + + + + + + + + + + } + label="Failed Logons Only" + /> + + {filterValues.failedLogonsOnly && ( + + + + )} + + + Apply Filter + + + + + ); + return ( <> > ); diff --git a/src/pages/index.js b/src/pages/index.js index b8ffad358dd8..0c60b06fe5a2 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -135,7 +135,6 @@ const Page = () => { if (!Array.isArray(actions)) { actions = [actions]; } - console.log("actions is", actions); actions.forEach((actionObj) => { if (actionObj?.value === "Remediate") { remediateCount++; @@ -260,7 +259,7 @@ const Page = () => { label: "", value: domain.name, }))} - actionItems={ + actionButton={ organization.data?.verifiedDomains?.length > 3 && ( setDomainVisible(!domainVisible)}> {domainVisible ? "See less" : "See more..."} diff --git a/src/pages/security/incidents/list-incidents/index.js b/src/pages/security/incidents/list-incidents/index.js index 923e2a722716..bebae5e2f983 100644 --- a/src/pages/security/incidents/list-incidents/index.js +++ b/src/pages/security/incidents/list-incidents/index.js @@ -82,6 +82,7 @@ const Page = () => { { const getInputParams = () => { if (values.command.value.requiresInput) { return { - [values.command.value.inputName]: values[values.command.value.inputName], + InputValue: values[values.command.value.inputName], }; } return {}; @@ -521,7 +521,7 @@ const AlertWizard = () => { {commandValue?.value?.requiresInput && ( { return ( setExpanded(!expanded)}> }> @@ -95,7 +96,7 @@ const Page = () => { // Filter for showing only pending requests { filterName: "Pending requests", - value: [{ id: "requestStatus", value: "Pending" }], + value: [{ id: "requestStatus", value: "InProgress" }], type: "column", }, ]} diff --git a/src/pages/tenant/administration/graph-explorer/index.js b/src/pages/tenant/administration/graph-explorer/index.js index 2e984b4c9bdd..05d03a30b75b 100644 --- a/src/pages/tenant/administration/graph-explorer/index.js +++ b/src/pages/tenant/administration/graph-explorer/index.js @@ -22,7 +22,7 @@ const Page = () => { } title={pageTitle} apiDataKey="Results" - apiUrl={apiFilter.endpoint ? "/api/ListGraphRequest" : null} + apiUrl={apiFilter.endpoint ? "/api/ListGraphRequest" : "/api/ListEmptyResults"} apiData={apiFilter} queryKey={queryKey} /*Key={`${apiFilter.endpoint}-${apiFilter.$select}`}*/ diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js index e04846851b4d..cb6a58d3d675 100644 --- a/src/pages/tenant/gdap-management/onboarding/start.js +++ b/src/pages/tenant/gdap-management/onboarding/start.js @@ -206,8 +206,8 @@ const Page = () => { var missingDefaults = []; cippDefaults.forEach((defaultRole) => { - if (!relationshipRoles?.find((role) => defaultRole.value === role.roleDefinitionId)) { - missingDefaults.push(role); + if (!relationshipRoles?.find((role) => defaultRole?.value === role?.roleDefinitionId)) { + missingDefaults.push(defaultRole); } }); setMissingDefaults(missingDefaults.length > 0); @@ -409,7 +409,9 @@ const Page = () => { {(currentInvite || selectedRole) && rolesMissingFromRelationship.length > 0 && ( The following roles are not mapped with the current template:{" "} - {rolesMissingFromRelationship.map((role) => role.Name).join(", ")} + {rolesMissingFromRelationship + .map((role) => role?.Name ?? "Unknown Role") + .join(", ")} )} {(currentInvite || selectedRole) && diff --git a/src/pages/tenant/standards/bpa-report/view.js b/src/pages/tenant/standards/bpa-report/view.js index b7db6c926e4e..2760fdc0a446 100644 --- a/src/pages/tenant/standards/bpa-report/view.js +++ b/src/pages/tenant/standards/bpa-report/view.js @@ -9,6 +9,7 @@ import { SvgIcon, Skeleton, Chip, + Alert, } from "@mui/material"; import Head from "next/head"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; @@ -22,6 +23,7 @@ import { CippImageCard } from "../../../../components/CippCards/CippImageCard"; import _ from "lodash"; const Page = () => { const router = useRouter(); + const { id } = router.query; const [blockCards, setBlockCards] = useState([]); const [layoutMode, setLayoutMode] = useState("Table"); const bpaTemplateList = ApiGetCall({ @@ -33,8 +35,9 @@ const Page = () => { url: "/api/listBPA", data: { tenantFilter: tenantFilter, + report: id, }, - queryKey: "ListBPA", + queryKey: `ListBPA-${id}-${tenantFilter}`, }); const tenantInfo = ApiGetCall({ url: "/api/ListTenants", @@ -51,7 +54,6 @@ const Page = () => { setLayoutMode(bpaTemplate.Style); if (bpaTemplate.Style === "Tenant") { const frontendFields = bpaTemplate.Data.map((block) => block.FrontendFields[0]); - if (bpaData.isSuccess) { const tenantId = tenantInfo?.data.find( (tenant) => tenant?.defaultDomainName === tenantFilter @@ -62,7 +64,7 @@ const Page = () => { //instead of this, use lodash to get the data for blockData const blockData = _.get(tenantData, field.value) ? _.get(tenantData, field.value) - : ["No Data"]; + : undefined; return { name: field.name, value: field.value, @@ -92,21 +94,12 @@ const Page = () => { //sometimes the subField contains a space. Only take the first part of the subField if it does. subField?.value?.includes(" ") ? subField.value.split(" ")[0] : subField.value ); + tenantData = Array.isArray(tenantData) ? tenantData : [tenantData]; //filter down tenantData to only the fields listOfFrontEndFields tenantData = tenantData.map((data) => { - const filteredData = {}; listOfFrontEndFields.unshift("Tenant"); - listOfFrontEndFields.forEach((field) => { - //we need to get the correct key, but the key is nested and can contain dots, or []. So we use lodash get to get the correct key. - const dataField = _.get(data, field) ? _.get(data, field) : "No Data"; - if (dataField === "FAILED") { - filteredData[field] = "Failed"; - } else { - filteredData[field] = dataField; - } - }); - return filteredData; + return data; }); const cards = { simpleColumns: listOfFrontEndFields, @@ -189,7 +182,13 @@ const Page = () => { } > - {block.formatter === "String" ? ( + {block.data === undefined ? ( + + No data has been found for this item. This tenant might not be licensed + for this feature, or data collection failed. Please check the logs for + more information. + + ) : block.formatter === "String" ? ( {block.data} diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index 68f46ba5a249..e041ea9cd23b 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -118,6 +118,7 @@ const Page = () => { "updatedAt", "updatedBy", "runManually", + "standards", ]} queryKey="listStandardTemplates" /> diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index d71adc70227c..2a5934014bc6 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -92,6 +92,14 @@ const Page = () => { }; const handleAddMultipleStandard = (standardName) => { + //if the standardname contains an array qualifier,e.g standardName[0], strip that away. + const arrayPattern = /(.*)\[(\d+)\]$/; + const match = standardName.match(arrayPattern); + if (match) { + standardName = match[1]; + } + console.log("Adding multiple", standardName); + setSelectedStandards((prev) => { const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName)); const newIndex = existingInstances.length; diff --git a/src/pages/tenant/tools/tenantlookup/index.js b/src/pages/tenant/tools/tenantlookup/index.js index 5c212c3d0751..ff8d73863b50 100644 --- a/src/pages/tenant/tools/tenantlookup/index.js +++ b/src/pages/tenant/tools/tenantlookup/index.js @@ -18,7 +18,7 @@ import { ApiGetCall } from "../../../../api/ApiCall"; const Page = () => { const formControl = useForm({ mode: "onBlur" }); const domain = useWatch({ control: formControl.control, name: "domain" }); - const getGeoIP = ApiGetCall({ + const getTenant = ApiGetCall({ url: "/api/ListExternalTenantInfo", data: { tenant: domain }, queryKey: `tenant-${domain}`, @@ -52,7 +52,7 @@ const Page = () => { getGeoIP.refetch()} + onClick={() => getTenant.refetch()} variant="contained" startIcon={} > @@ -64,7 +64,7 @@ const Page = () => { {/* Results Card */} - {getGeoIP.isFetching ? ( + {getTenant.isFetching ? ( @@ -74,25 +74,25 @@ const Page = () => { - ) : getGeoIP.data ? ( + ) : getTenant.data ? ( - + Tenant Name: {domain} - Tenant Id: {getGeoIP.data?.GraphRequest?.tenantId} + Tenant Id: {getTenant.data?.GraphRequest?.tenantId} Default Domain Name:{" "} - {getGeoIP.data?.GraphRequest?.defaultDomainName} + {getTenant.data?.GraphRequest?.defaultDomainName} Tenant Brand Name :{" "} - {getGeoIP.data?.GraphRequest?.federationBrandName - ? getGeoIP.data?.GraphRequest?.federationBrandName + {getTenant.data?.GraphRequest?.federationBrandName + ? getTenant.data?.GraphRequest?.federationBrandName : "N/A"} @@ -101,7 +101,7 @@ const Page = () => { domains: - {getGeoIP.data?.Domains?.map((domain) => ( + {getTenant.data?.Domains?.map((domain) => ( {domain} diff --git a/src/pages/tools/breachlookup/index.js b/src/pages/tools/breachlookup/index.js index 344ef279d790..d50e98f137a7 100644 --- a/src/pages/tools/breachlookup/index.js +++ b/src/pages/tools/breachlookup/index.js @@ -8,6 +8,7 @@ import { Link, Chip, Avatar, + Alert, } from "@mui/material"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; @@ -54,6 +55,12 @@ const Page = () => { + + + This page is in beta and may not always give expected results. + + + @@ -234,7 +241,6 @@ const Page = () => { )} - {console.log(getGeoIP.error)} diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 6bac0ef28080..73c65b8dbdad 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -13,11 +13,13 @@ import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClip import { getCippLicenseTranslation } from "./get-cipp-license-translation"; import CippDataTableButton from "../components/CippTable/CippDataTableButton"; import { LinearProgressWithLabel } from "../components/linearProgressWithLabel"; +import { CippLocationDialog } from "../components/CippComponents/CippLocationDialog"; import { isoDuration, en } from "@musement/iso-duration"; import { CippTimeAgo } from "../components/CippComponents/CippTimeAgo"; import { getCippRoleTranslation } from "./get-cipp-role-translation"; import { CogIcon, ServerIcon, UserIcon, UsersIcon } from "@heroicons/react/24/outline"; import { getCippTranslation } from "./get-cipp-translation"; +import { getSignInErrorCodeTranslation } from "./get-cipp-signin-errorcode-translation"; export const getCippFormatting = (data, cellName, type, canReceive) => { const isText = type === "text"; @@ -59,6 +61,20 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ); } + if (cellName === "prohibitSendReceiveQuotaInBytes" || cellName === "storageUsedInBytes") { + //convert bytes to GB + const bytes = data; + if (bytes === null || bytes === undefined) { + return isText ? ( + "No data" + ) : ( + + ); + } + const gb = bytes / 1024 / 1024 / 1024; + return isText ? `${gb.toFixed(2)} GB` : `${gb.toFixed(2)} GB`; + } + if (cellName === "info.logoUrl") { return isText ? ( data @@ -94,6 +110,8 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { "purchaseDate", "NextOccurrence", "LastOccurrence", + "NotBefore", + "NotAfter", ]; const matchDateTime = /[dD]ate[tT]ime/; @@ -187,8 +205,8 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ? data.join(", ") : data.map((item) => ( )); @@ -196,7 +214,7 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { return isText ? ( data ) : ( - + ); } } @@ -216,9 +234,13 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { } if (data?.enabled === true && data?.date) { - return isText - ? `Yes, Scheduled for ${new Date(data.date).toLocaleString()}` - : `Yes, Scheduled for ${new Date(data.date).toLocaleString()}`; + return isText ? ( + `Yes, Scheduled for ${new Date(data.date).toLocaleString()}` + ) : ( + <> + Yes, Scheduled for + > + ); } if (data?.enabled === true || data?.enabled === false) { return isText ? ( @@ -337,13 +359,15 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ); } - const translateProps = [ - "riskLevel", - "riskState", - "riskDetail", - "enrollmentType", - "profileType", - ]; + if (cellName === "status.errorCode") { + return getSignInErrorCodeTranslation(data); + } + + if (cellName === "location" && data?.geoCoordinates) { + return isText ? JSON.stringify(data) : ; + } + + const translateProps = ["riskLevel", "riskState", "riskDetail", "enrollmentType", "profileType"]; if (translateProps.includes(cellName)) { return getCippTranslation(data); diff --git a/src/utils/get-cipp-signin-errorcode-translation.js b/src/utils/get-cipp-signin-errorcode-translation.js new file mode 100644 index 000000000000..00f0074e05f5 --- /dev/null +++ b/src/utils/get-cipp-signin-errorcode-translation.js @@ -0,0 +1,9 @@ +import SignInErrorCodes from "/src/data/signinErrorCodes"; + +export const getSignInErrorCodeTranslation = (errorCode) => { + if (SignInErrorCodes.hasOwnProperty(String(errorCode))) { + return SignInErrorCodes[String(errorCode)]; + } else { + return errorCode; + } +};
This is a placeholder page for the add profile section.