diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index a0bab7612a02..aa7fc42a8bca 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -16,7 +16,7 @@ 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) => { @@ -145,14 +145,18 @@ export const CippBannerListCard = (props) => { {isCollapsible && ( - {item?.propertyItems?.length > 0 && ( - - )} - {item?.table && } + + {item?.propertyItems?.length > 0 && ( + + )} + {item?.table && } + {item?.children && {item.children}} + {item?.actionButton && {item.actionButton}} + )} @@ -180,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/CippComponents/CippLocationDialog.jsx b/src/components/CippComponents/CippLocationDialog.jsx new file mode 100644 index 000000000000..9970524ed681 --- /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 Details + + + + + + + + + + ); +}; diff --git a/src/components/CippTable/util-columnsFromAPI.js b/src/components/CippTable/util-columnsFromAPI.js index 886a6f1d258b..e783ebe16133 100644 --- a/src/components/CippTable/util-columnsFromAPI.js +++ b/src/components/CippTable/util-columnsFromAPI.js @@ -30,12 +30,17 @@ const mergeKeys = (dataArray) => { export const utilColumnsFromAPI = (dataArray) => { const dataSample = mergeKeys(dataArray); - + const skipRecursion = ["location"]; const generateColumns = (obj, parentKey = "") => { 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/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/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index a9d325f8f35a..bef693434d29 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,6 +166,79 @@ const Page = () => { })) || [], }, ]; + + const mailboxRuleActions = [ + { + label: "Remove Mailbox Rule", + type: "GET", + icon: , + url: "/api/ExecRemoveMailboxRule", + data: { ruleId: "Identity", userPrincipalName: "UserPrincipalName" }, + confirmText: "Are you sure you want to remove this mailbox rule?", + multiPost: false, + }, + ]; + + 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 || [], + simpleColumns: [ + "Enabled", + "Name", + "Description", + "Redirect To", + "Copy To Folder", + "Move To Folder", + "Soft Delete Message", + "Delete Message", + ], + 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 ( { 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); @@ -112,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: ( + + ), propertyItems: [ { label: "Client App Used", @@ -131,6 +191,39 @@ const Page = () => { value: signInData.status?.additionalDetails || "N/A", }, ], + children: ( + <> + {signInData?.location && ( + <> + Location + + + + + + + + + + )} + + ), }; // Prepare the conditional access policies items @@ -459,6 +552,12 @@ const Page = () => { )} + setSignInLogsDialogOpen(false)} + userId={userId} + tenantFilter={userSettingsDefaults.currentTenant} + /> ); }; 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 && ( + + + + )} + + + + + + ); + return ( <> ); diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index d8e7ff471c6d..a11dea40b087 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"; @@ -353,6 +355,14 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ); } + if (cellName === "status.errorCode") { + return getSignInErrorCodeTranslation(data); + } + + if (cellName === "location") { + return isText ? JSON.stringify(data) : ; + } + const translateProps = ["riskLevel", "riskState", "riskDetail", "enrollmentType", "profileType"]; if (translateProps.includes(cellName)) { 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..10f0c257a4d1 --- /dev/null +++ b/src/utils/get-cipp-signin-errorcode-translation.js @@ -0,0 +1,9 @@ +import SignInErrorCodes from "../data/SignInErrorCodes.json"; + +export const getSignInErrorCodeTranslation = (errorCode) => { + if (SignInErrorCodes.hasOwnProperty(String(errorCode))) { + return SignInErrorCodes[String(errorCode)]; + } else { + return errorCode; + } +};