diff --git a/.github/workflows/azure-static-web-apps-polite-forest-010314b03.yml b/.github/workflows/azure-static-web-apps-polite-forest-010314b03.yml new file mode 100644 index 000000000000..59c26e998103 --- /dev/null +++ b/.github/workflows/azure-static-web-apps-polite-forest-010314b03.yml @@ -0,0 +1,43 @@ +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - main + + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v3 + with: + submodules: true + lfs: false + - 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_POLITE_FOREST_010314B03 }} + 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: "" # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: 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_POLITE_FOREST_010314B03 }} + action: "close" diff --git a/.gitignore b/.gitignore index 1f85272ad618..668afda8fa94 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,11 @@ out npm-debug.log* yarn-debug.log* yarn-error.log* +<<<<<<< HEAD +__* +.next/* +======= # vscode debug logs -debug.log \ No newline at end of file +debug.log +>>>>>>> 81ee51ffa0576b02dc60ee5600a44cec3cde3e97 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..38b71fec4e7f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "github.vscode-codeql", + "dbaeumer.vscode-eslint", + "christian-kohler.npm-intellisense", + "esbenp.prettier-vscode", + "stylelint.vscode-stylelint", + "editorconfig.editorconfig" + ] +} diff --git a/package.json b/package.json index 6c2c57ba925d..f24808847b21 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { "name": "cipp", +<<<<<<< HEAD + "version": "6.4.1", + "description": "The CyberDrain Improved Partner Portal is a portal to help manage administration for Microsoft Partners.", +======= "version": "7.0.0", "author": "CIPP Contributors", +>>>>>>> 81ee51ffa0576b02dc60ee5600a44cec3cde3e97 "homepage": "https://cipp.app/", "bugs": { "url": "https://github.com/KelvinTegelaar/CIPP/issues" @@ -103,8 +108,42 @@ "yup": "0.32.11" }, "devDependencies": { +<<<<<<< HEAD + "@types/react": "^18.2.39", + "@types/react-helmet": "^6.1.5", + "@vitejs/plugin-react": "^4.2.1", + "auto-changelog": "~2.3.0", + "browserslist-to-esbuild": "^1.2.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-custom-alias": "^1.3.2", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "husky": "^7.0.4", + "postcss-scss": "^4.0.3", + "prettier": "2.4.1", + "redux-immutable-state-invariant": "^2.1.0", + "sass": "^1.64.2", + "stylelint": "^14.3.0", + "stylelint-config-sass-guidelines": "^9.0.1", + "stylelint-order": "^5.0.0", + "vite": "^5.0.6", + "vite-plugin-eslint": "^1.8.1" + }, + "engines": { + "node": "20.17.0", + "npm": ">=8.3.0" + }, + "overrides": { + "react": "^18.2.0", + "react-dom": "^18.2.0" +======= "@svgr/webpack": "6.5.1", "eslint": "8.32.0", "eslint-config-next": "13.1.6" +>>>>>>> 81ee51ffa0576b02dc60ee5600a44cec3cde3e97 } } diff --git a/public/img/RoB-light.svg b/public/img/RoB-light.svg new file mode 100644 index 000000000000..0673b2a8a449 --- /dev/null +++ b/public/img/RoB-light.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/img/RoB.svg b/public/img/RoB.svg new file mode 100644 index 000000000000..d188e1eb541b --- /dev/null +++ b/public/img/RoB.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/version_latest.txt b/public/version_latest.txt index 5e39348ef037..2e0b229ff633 100644 --- a/public/version_latest.txt +++ b/public/version_latest.txt @@ -1 +1,5 @@ -99.99.99 \ No newline at end of file +<<<<<<< HEAD +6.4.1 +======= +99.99.99 +>>>>>>> 81ee51ffa0576b02dc60ee5600a44cec3cde3e97 diff --git a/src/components/layout/AppFooter.jsx b/src/components/layout/AppFooter.jsx new file mode 100644 index 000000000000..09087af3a00e --- /dev/null +++ b/src/components/layout/AppFooter.jsx @@ -0,0 +1,56 @@ +import React from 'react' +import { CFooter, CImage, CLink } from '@coreui/react' +import { Link } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { useMediaPredicate } from 'react-media-hook' + +const AppFooter = () => { + const currentTheme = useSelector((state) => state.app.currentTheme) + const preferredTheme = useMediaPredicate('(prefers-color-scheme: dark)') ? 'impact' : 'cyberdrain' + const isDark = + currentTheme === 'impact' || (currentTheme === 'default' && preferredTheme === 'impact') + + const RoB = isDark ? '/img/RoB.svg' : '/img/RoB-light.svg' + const huntress = isDark ? '/img/huntress_teal.png' : '/img/huntress_teal.png' + const rewst = isDark ? '/img/rewst_dark.png' : '/img/rewst.png' + const ninjaone = isDark ? '/img/ninjaone_dark.png' : '/img/ninjaone.png' + const augmentt = isDark ? '/img/augmentt-dark.png' : '/img/augmentt-light.png' + + return ( + +
+

+ This application is sponsored by + + + + + + + + + + + + + + + +

+
+ +
+ ) +} + +export default React.memo(AppFooter) diff --git a/src/components/tables/CippDatatable.jsx b/src/components/tables/CippDatatable.jsx new file mode 100644 index 000000000000..7d00d9730e82 --- /dev/null +++ b/src/components/tables/CippDatatable.jsx @@ -0,0 +1,68 @@ +import React from 'react' +import { useListDatatableQuery } from 'src/store/api/datatable' +import PropTypes from 'prop-types' +import { CippTable } from 'src/components/tables' +import { CippTablePropTypes } from 'src/components/tables/CippTable' +import { CCallout } from '@coreui/react' + +export default function CippDatatable({ path, params, ...rest }) { + const [graphFilter, setGraphFilter] = React.useState(params?.Parameters?.$filter) + const { + data = [], + isFetching, + error, + refetch, + } = useListDatatableQuery({ path, params: { $filter: graphFilter, ...params } }) + + let anonymized = false // Assuming default value is false + const regex = new RegExp('^[A-Z0-9]+$') + const principalNameOrUPN = + data[0]?.userPrincipalName ?? + data[0]?.UPN ?? + data[0]?.Owner ?? + data.Results?.[0]?.upn ?? + data.Results?.[0]?.userPrincipalName ?? + data.Results?.[0]?.Owner + + if (principalNameOrUPN && regex.test(principalNameOrUPN)) { + anonymized = true + } + + var defaultFilterText = '' + if (params?.Parameters?.$filter) { + defaultFilterText = 'Graph: ' + params?.Parameters?.$filter + } + return ( + <> + {anonymized && ( + + This table might contain anonymized data. Please check this + + documentation link + + to resolve this. + + )} + {data?.Metadata?.Queued && {data?.Metadata?.QueueMessage}} + refetch()} + graphFilterFunction={setGraphFilter} + /> + + ) +} + +CippDatatable.propTypes = { + path: PropTypes.string.isRequired, + params: PropTypes.object, + ...CippTablePropTypes, +} diff --git a/src/components/utilities/CippAppPermissionBuilder.jsx b/src/components/utilities/CippAppPermissionBuilder.jsx new file mode 100644 index 000000000000..8959b4f826fa --- /dev/null +++ b/src/components/utilities/CippAppPermissionBuilder.jsx @@ -0,0 +1,1019 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react' +import { + CButton, + CCallout, + CCol, + CForm, + CRow, + CAccordion, + CAccordionHeader, + CAccordionBody, + CAccordionItem, + CTooltip, +} from '@coreui/react' +import { Field, Form, FormSpy } from 'react-final-form' +import { RFFCFormRadioList, RFFSelectSearch } from 'src/components/forms' +import { useGenericGetRequestQuery, useLazyGenericGetRequestQuery } from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + TenantSelectorMultiple, + ModalService, + CippOffcanvas, + CippCodeBlock, +} from 'src/components/utilities' +import PropTypes from 'prop-types' +import { OnChange } from 'react-final-form-listeners' +import { useListTenantsQuery } from 'src/store/api/tenants' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' +import { CippTable } from '../tables' +import { Row } from 'react-bootstrap' +import { cellGenericFormatter } from '../tables/CellGenericFormat' +import Skeleton from 'react-loading-skeleton' +import CippDropzone from './CippDropzone' +import { Editor } from '@monaco-editor/react' +import { useSelector } from 'react-redux' +import { CippCallout } from '../layout' + +const CippAppPermissionBuilder = ({ + onSubmit, + currentPermissions = {}, + isSubmitting, + colSize = 8, + removePermissionConfirm = true, + appDisplayName = 'CIPP-SAM', +}) => { + const [selectedApp, setSelectedApp] = useState([]) + const [permissionsImported, setPermissionsImported] = useState(false) + const [newPermissions, setNewPermissions] = useState({}) + const [importedManifest, setImportedManifest] = useState(null) + const [manifestVisible, setManifestVisible] = useState(false) + const currentTheme = useSelector((state) => state.app.currentTheme) + const [calloutMessage, setCalloutMessage] = useState(null) + const [initialPermissions, setInitialPermissions] = useState() + + const { + data: servicePrincipals = [], + isFetching: spFetching, + isSuccess: spSuccess, + isUninitialized: spUninitialized, + refetch: refetchSpList, + } = useGenericGetRequestQuery({ + path: 'api/ExecServicePrincipals', + }) + const [createServicePrincipal, createResult] = useLazyGenericGetRequestQuery() + + const removeServicePrincipal = (appId) => { + var servicePrincipal = selectedApp.find((sp) => sp?.appId === appId) + var newServicePrincipals = selectedApp.filter((sp) => sp?.appId !== appId) + + if (removePermissionConfirm) { + ModalService.confirm({ + title: 'Remove Service Principal', + body: `Are you sure you want to remove ${servicePrincipal.displayName}?`, + onConfirm: () => { + setSelectedApp(newServicePrincipals) + var updatedPermissions = JSON.parse(JSON.stringify(newPermissions)) + delete updatedPermissions.Permissions[appId] + setNewPermissions(updatedPermissions) + }, + }) + } else { + setSelectedApp(newServicePrincipals) + var updatedPermissions = JSON.parse(JSON.stringify(newPermissions)) + delete updatedPermissions.Permissions[appId] + setNewPermissions(updatedPermissions) + } + } + + const confirmReset = () => { + if (removePermissionConfirm) { + ModalService.confirm({ + title: 'Reset to Default', + body: 'Are you sure you want to reset all permissions to default?', + onConfirm: () => { + setSelectedApp([]) + setPermissionsImported(false) + setManifestVisible(false) + setCalloutMessage('Permissions reset to default.') + }, + }) + } else { + setSelectedApp([]) + setPermissionsImported(false) + setManifestVisible(false) + setCalloutMessage('Permissions reset to default.') + } + } + + const handleSubmit = (values) => { + if (onSubmit) { + var postBody = { + Permissions: newPermissions.Permissions, + } + onSubmit(postBody) + } + } + + const onCreateServicePrincipal = (appId) => { + createServicePrincipal({ + path: 'api/ExecServicePrincipals?Action=Create&AppId=' + appId, + }).then(() => { + refetchSpList() + setCalloutMessage(createResult?.data?.Results) + }) + } + + const addPermissionRow = (servicePrincipal, permissionType, permission) => { + var updatedPermissions = JSON.parse(JSON.stringify(newPermissions)) + + if (!updatedPermissions?.Permissions[servicePrincipal]) { + updatedPermissions.Permissions[servicePrincipal] = { + applicationPermissions: [], + delegatedPermissions: [], + } + } + var currentPermission = updatedPermissions?.Permissions[servicePrincipal][permissionType] + var newPermission = [] + if (currentPermission) { + currentPermission.map((perm) => { + if (perm.id !== permission.value) { + newPermission.push(perm) + } + }) + } + newPermission.push({ id: permission.value, value: permission.label }) + + updatedPermissions.Permissions[servicePrincipal][permissionType] = newPermission + setNewPermissions(updatedPermissions) + } + + const removePermissionRow = (servicePrincipal, permissionType, permissionId, permissionValue) => { + if (removePermissionConfirm) { + ModalService.confirm({ + title: 'Remove Permission', + body: `Are you sure you want to remove the permission: ${permissionValue}?`, + onConfirm: () => { + var updatedPermissions = JSON.parse(JSON.stringify(newPermissions)) + var currentPermission = updatedPermissions?.Permissions[servicePrincipal][permissionType] + var newPermission = [] + if (currentPermission) { + currentPermission.map((perm) => { + if (perm.id !== permissionId) { + newPermission.push(perm) + } + }) + } + updatedPermissions.Permissions[servicePrincipal][permissionType] = newPermission + setNewPermissions(updatedPermissions) + }, + }) + } else { + var updatedPermissions = JSON.parse(JSON.stringify(newPermissions)) + var currentPermission = updatedPermissions?.Permissions[servicePrincipal][permissionType] + var newPermission = [] + if (currentPermission) { + currentPermission.map((perm) => { + if (perm.id !== permissionId) { + newPermission.push(perm) + } + }) + } + updatedPermissions.Permissions[servicePrincipal][permissionType] = newPermission + setNewPermissions(updatedPermissions) + } + } + + const generateManifest = ({ appDisplayName = 'CIPP-SAM', prompt = false }) => { + if (prompt || appDisplayName === '') { + ModalService.prompt({ + title: 'Generate Manifest', + body: 'Please enter the display name for the application.', + onConfirm: (value) => { + generateManifest({ appDisplayName: value }) + }, + }) + } else { + var manifest = { + isFallbackPublicClient: true, + signInAudience: 'AzureADMultipleOrgs', + displayName: appDisplayName, + web: { + redirectUris: [ + 'https://login.microsoftonline.com/common/oauth2/nativeclient', + 'https://localhost', + 'http://localhost', + 'http://localhost:8400', + ], + }, + requiredResourceAccess: [], + } + + var additionalPermissions = [] + + selectedApp.map((sp) => { + var appRoles = newPermissions?.Permissions[sp.appId]?.applicationPermissions + var delegatedPermissions = newPermissions?.Permissions[sp.appId]?.delegatedPermissions + var requiredResourceAccess = { + resourceAppId: sp.appId, + resourceAccess: [], + } + var additionalRequiredResourceAccess = { + resourceAppId: sp.appId, + resourceAccess: [], + } + if (appRoles) { + appRoles.map((role) => { + requiredResourceAccess.resourceAccess.push({ + id: role.id, + type: 'Role', + }) + }) + } + if (delegatedPermissions) { + delegatedPermissions.map((perm) => { + // permission not a guid skip + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(perm.id)) { + requiredResourceAccess.resourceAccess.push({ + id: perm.id, + type: 'Scope', + }) + } else { + additionalRequiredResourceAccess.resourceAccess.push({ + id: perm.id, + type: 'Scope', + }) + } + }) + } + if (requiredResourceAccess.resourceAccess.length > 0) { + manifest.requiredResourceAccess.push(requiredResourceAccess) + } + if (additionalRequiredResourceAccess.resourceAccess.length > 0) { + additionalPermissions.push(additionalRequiredResourceAccess) + } + }) + + var fileName = `${appDisplayName.replace(' ', '-')}.json` + if (appDisplayName === 'CIPP-SAM') { + fileName = 'SAMManifest.json' + } + + var blob = new Blob([JSON.stringify(manifest, null, 2)], { type: 'application/json' }) + var url = URL.createObjectURL(blob) + var a = document.createElement('a') + a.href = url + a.download = `${fileName}` + a.click() + URL.revokeObjectURL(url) + + if (additionalPermissions.length > 0) { + ModalService.confirm({ + title: 'Additional Permissions', + body: 'Some permissions are not supported in the manifest. Would you like to download them?', + confirmLabel: 'Download', + onConfirm: () => { + var additionalBlob = new Blob([JSON.stringify(additionalPermissions, null, 2)], { + type: 'application/json', + }) + var additionalUrl = URL.createObjectURL(additionalBlob) + var additionalA = document.createElement('a') + additionalA.href = additionalUrl + additionalA.download = 'AdditionalPermissions.json' + additionalA.click() + URL.revokeObjectURL(additionalUrl) + }, + }) + } + } + } + + const importManifest = () => { + var updatedPermissions = { Permissions: {} } + var manifest = importedManifest + var requiredResourceAccess = manifest.requiredResourceAccess + var selectedServicePrincipals = [] + + requiredResourceAccess.map((resourceAccess) => { + var sp = servicePrincipals?.Results?.find((sp) => sp.appId === resourceAccess.resourceAppId) + if (sp) { + var appRoles = [] + var delegatedPermissions = [] + selectedServicePrincipals.push(sp) + resourceAccess.resourceAccess.map((access) => { + if (access.type === 'Role') { + var role = sp.appRoles.find((role) => role.id === access.id) + if (role) { + appRoles.push({ + id: role.id, + value: role.value, + }) + } + } else if (access.type === 'Scope') { + var scope = sp.publishedPermissionScopes.find((scope) => scope.id === access.id) + if (scope) { + delegatedPermissions.push({ + id: scope.id, + value: scope.value, + }) + } + } + }) + updatedPermissions.Permissions[sp.appId] = { + applicationPermissions: appRoles, + delegatedPermissions: delegatedPermissions, + } + } + }) + setNewPermissions(updatedPermissions) + setSelectedApp(selectedServicePrincipals) + setImportedManifest(null) + setPermissionsImported(true) + setManifestVisible(false) + setCalloutMessage('Manifest imported successfully.') + } + + const onManifestImport = useCallback((acceptedFiles) => { + acceptedFiles.forEach((file) => { + const reader = new FileReader() + reader.onabort = () => console.log('file reading was aborted') + reader.onerror = () => console.log('file reading has failed') + reader.onload = () => { + console.log(reader.result) + try { + var manifest = JSON.parse(reader.result) + setImportedManifest(manifest) + console.log(importedManifest) + } catch { + console.log('invalid manifest') + } + } + reader.readAsText(file) + }) + }, []) + + useEffect(() => { + try { + var initialAppIds = Object.keys(currentPermissions?.Permissions) + } catch { + initialAppIds = [] + } + + if (spSuccess && selectedApp.length == 0 && initialAppIds.length == 0) { + var microsoftGraph = servicePrincipals?.Results?.find( + (sp) => sp?.appId === '00000003-0000-0000-c000-000000000000', + ) + setSelectedApp([microsoftGraph]) + setNewPermissions({ + Permissions: { + '00000003-0000-0000-c000-000000000000': { + applicationPermissions: [], + delegatedPermissions: [], + }, + }, + }) + } else if (spSuccess & (currentPermissions !== initialPermissions)) { + setSelectedApp([]) + setNewPermissions(currentPermissions) + setInitialPermissions(currentPermissions) + setPermissionsImported(false) + } else if (spSuccess && initialAppIds.length > 0 && permissionsImported == false) { + var newApps = [] + initialAppIds?.map((appId) => { + var newSp = servicePrincipals?.Results?.find((sp) => sp.appId === appId) + if (newSp) { + newApps.push(newSp) + } + }) + newApps = newApps.sort((a, b) => { + return a.displayName.localeCompare(b.displayName) + }) + setSelectedApp(newApps) + setNewPermissions(currentPermissions) + setInitialPermissions(currentPermissions) + setPermissionsImported(true) + } + }, [ + currentPermissions, + initialPermissions, + permissionsImported, + spSuccess, + selectedApp, + servicePrincipals, + setSelectedApp, + setPermissionsImported, + setNewPermissions, + setInitialPermissions, + ]) + + const ApiPermissionRow = ({ servicePrincipal = null }) => { + const { + data: servicePrincipalData = [], + isFetching: spFetching, + isSuccess: spIdSuccess, + } = useGenericGetRequestQuery({ + path: 'api/ExecServicePrincipals?Id=' + servicePrincipal.id, + }) + + return ( + <> + {spSuccess && servicePrincipal !== null && spIdSuccess && ( + + + + +

+ Manage the permissions for the {servicePrincipal.displayName}. +

+
+ + +
+ { + removeServicePrincipal(servicePrincipal.appId) + }} + disabled={servicePrincipal.appId === '00000003-0000-0000-c000-000000000000'} + className={`circular-button`} + > + + +
+
+
+
+ + + {servicePrincipal?.appRoles?.length > 0 ? ( + <> + + + {({ values }) => { + return ( + + + { + return newPermissions?.Permissions[ + servicePrincipal.appId + ]?.applicationPermissions?.find( + (perm) => perm.id === role.id, + ) + ? false + : true + }) + .map((role) => ({ + name: role.value, + value: role.id, + })) + .sort((a, b) => a.name.localeCompare(b.name))} + /> + + + + { + addPermissionRow( + servicePrincipal.appId, + 'applicationPermissions', + values.Permissions[servicePrincipal?.appId] + .applicationPermissions, + ) + values.Permissions[ + servicePrincipal.appId + ].applicationPermissions = '' + }} + className={`circular-button`} + > + + + + + + ) + }} + + +
+ row.value, + name: 'Permission', + sortable: true, + exportSelector: 'value', + maxWidth: '30%', + cell: cellGenericFormatter(), + }, + { + selector: (row) => row.id, + name: 'Id', + omit: true, + exportSelector: 'id', + }, + { + selector: (row) => + servicePrincipalData?.Results?.appRoles.find( + (role) => role.id === row.id, + ).description, + name: 'Description', + cell: cellGenericFormatter({ wrap: true }), + maxWidth: '60%', + }, + { + name: 'Actions', + cell: (row) => { + return ( + + { + removePermissionRow( + servicePrincipal.appId, + 'applicationPermissions', + row.id, + row.value, + ) + }} + color="danger" + variant="ghost" + size="sm" + > + + + + ) + }, + maxWidth: '10%', + }, + ]} + dynamicColumns={false} + /> +
+ + ) : ( + <> + + + No Application Permissions found. + + + )} +
+
+ + +
+
+
+ + + {servicePrincipalData?.Results?.publishedPermissionScopes?.length == 0 && ( + + + No Published Delegated Permissions found. + + )} + + {({ values }) => { + return ( + + + 0 + ? servicePrincipalData?.Results?.publishedPermissionScopes + .filter((scopes) => { + return newPermissions?.Permissions[ + servicePrincipal.appId + ]?.delegatedPermissions?.find( + (perm) => perm.id === scopes.id, + ) + ? false + : true + }) + .map((scope) => ({ + name: scope.value, + value: scope.id, + })) + .sort((a, b) => a.name.localeCompare(b.name)) + : [] + } + allowCreate={true} + /> + + + + { + addPermissionRow( + servicePrincipal.appId, + 'delegatedPermissions', + values?.Permissions[servicePrincipal.appId] + .delegatedPermissions, + ) + values.Permissions[servicePrincipal.appId].delegatedPermissions = + '' + }} + className={`circular-button`} + > + + + + + + ) + }} + + +
+ row?.value, + name: 'Permission', + sortable: true, + exportSelector: 'value', + maxWidth: '30%', + cell: cellGenericFormatter(), + }, + { + selector: (row) => row?.id, + name: 'Id', + omit: true, + exportSelector: 'id', + }, + { + selector: (row) => + servicePrincipalData?.Results?.publishedPermissionScopes.find( + (scope) => scope?.id === row?.id, + )?.userConsentDescription ?? 'No Description', + name: 'Description', + cell: cellGenericFormatter({ wrap: true }), + maxWidth: '60%', + }, + { + name: 'Actions', + cell: (row) => { + return ( + + { + removePermissionRow( + servicePrincipal.appId, + 'delegatedPermissions', + row.id, + row.value, + ) + }} + color="danger" + variant="ghost" + size="sm" + > + + + + ) + }, + maxWidth: '10%', + }, + ]} + dynamicColumns={false} + /> +
+
+
+
+
+ )} + + ) + } + ApiPermissionRow.propTypes = { + servicePrincipal: PropTypes.object, + } + + return ( + <> + {spFetching && } + {spSuccess && ( +
{ + return ( + + + + + + {servicePrincipals?.Metadata?.Success && ( + ({ + name: `${sp.displayName} (${sp.appId})`, + value: sp.appId, + }))} + isLoading={spFetching} + refreshFunction={() => refetchSpList()} + allowCreate={true} + onCreateOption={(newSp) => { + onCreateServicePrincipal(newSp) + }} + placeholder="(Advanced) Select a Service Principal" + /> + )} + + + + {({ values }) => { + return ( + + + setSelectedApp([ + ...selectedApp, + servicePrincipals?.Results?.find( + (sp) => sp.appId === values.servicePrincipal.value, + ), + ]) + } + disabled={!values.servicePrincipal} + className={`circular-button`} + title={'+'} + > + + + + ) + }} + + + { + confirmReset() + }} + className={`circular-button`} + title={'+'} + > + + + + + { + generateManifest({ appDisplayName: appDisplayName }) + }} + className={`circular-button`} + title={'+'} + > + + + + + + { + setManifestVisible(true) + }} + className={`circular-button`} + title={'+'} + > + + + + + + { + setManifestVisible(false) + }} + addedClass="offcanvas-large" + placement="end" + > + + +

+ Import a JSON application manifest to set permissions. This will + overwrite any existing permissions. +

+
+
+ + + + + + {importedManifest && ( + <> + + + importManifest()}> + Import + + + + + +

Preview

+ +
+
+ + )} +
+ {calloutMessage && ( + + + + + {calloutMessage} + + + + )} + + {newPermissions?.MissingPermissions && + newPermissions?.Type === 'Table' && + Object.keys(newPermissions?.MissingPermissions).length > 0 && ( + + + + + + + New Permissions Available + {Object.keys(newPermissions?.MissingPermissions).map((perm) => { + // translate appid to display name + var sp = servicePrincipals?.Results?.find( + (sp) => sp.appId === perm, + ) + return ( +
+ {sp?.displayName}:{' '} + {Object.keys(newPermissions?.MissingPermissions[perm]).map( + (type) => { + return ( + <> + {newPermissions?.MissingPermissions[perm][type] + .length > 0 && ( + + {type == 'applicationPermissions' + ? 'Application' + : 'Delegated'}{' '} + -{' '} + {newPermissions?.MissingPermissions[perm][type] + .map((p) => { + return p.value + }) + .join(', ')} + + )} + + ) + }, + )} +
+ ) + })} +
+ + + { + var updatedPermissions = JSON.parse( + JSON.stringify(newPermissions), + ) + Object.keys(newPermissions?.MissingPermissions).map( + (perm) => { + Object.keys( + newPermissions?.MissingPermissions[perm], + ).map((type) => { + if (!updatedPermissions.Permissions[perm][type]) { + updatedPermissions.Permissions[perm][type] = [] + } + newPermissions?.MissingPermissions[perm][type].map( + (p) => { + updatedPermissions.Permissions[perm][type].push(p) + }, + ) + }) + }, + ) + updatedPermissions.MissingPermissions = {} + setNewPermissions(updatedPermissions) + }} + className={`circular-button float-end`} + > + + + + +
+
+
+
+ )} + + <> + {selectedApp?.length > 0 && + selectedApp?.map((sp, spIndex) => ( + + {sp.displayName} + + + + + + + ))} + + +
+ + + + {({ values }) => { + return <> + }} + + +
+ + + + + + Save + + + + +
+ ) + }} + /> + )} + + ) +} +CippAppPermissionBuilder.propTypes = { + onSubmit: PropTypes.func, + currentPermissions: PropTypes.object, + isSubmitting: PropTypes.bool, + colSize: PropTypes.number, + removePermissionConfirm: PropTypes.bool, + appDisplayName: PropTypes.string, +} + +export default CippAppPermissionBuilder diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss new file mode 100644 index 000000000000..2b1e6c2aa248 --- /dev/null +++ b/src/scss/_custom.scss @@ -0,0 +1,713 @@ +@use 'sass:color'; +.custom-switch-wrapper { + position: relative; + display: inline-block; +} +body { + overflow-y: scroll; // Always show vertical scrollbar +} +.image-upload-container { + position: relative; + width: 100px; /* Adjusted to match the image */ + height: 120px; /* Adjusted to match the image */ + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.upload-image { + width: 100%; + height: 100%; + border-radius: 8px; + object-fit: cover; /* Ensures the image covers the area without stretching */ +} + +.image-upload-overlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + transition: 0.5s ease; + background-color: var(--cyberdrain-secondary); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; +} + +.image-upload-container:hover .image-upload-overlay { + opacity: 0.8; +} + +.overlay-text { + color: white; + font-size: 20px; +} + +.circular-button { + border-radius: 50% !important; + width: 35px; + height: 35px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: bold; + margin-right: 1rem; +} + +.round-focus { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 21, 0.075), + 0 0 0 0.25rem var(--cui-btn-shadow); +} + +.circular-button.active { + background-color: #4caf50; + color: #ffffff; +} +.circular-button.default { + background-color: var(--cui-primary); + color: var(--text-primary); +} + +.circular-button.cyberdrain { + background-color: var(--cyberdrain-accent-beige); + color: var(--text-primary); +} + +.circular-button.impact { + background-color: var(--cyberdrain-secondary); + color: var(--text-primary); +} + +h3.underline:after { + background: none repeat scroll 0 0 var(--cipp-table-context-bg); + bottom: -13px; + content: ''; + display: block; + height: 5px; + position: relative; + width: 2.5rem; +} + +.switch-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; /* Ensures the icon doesn't interfere with the switch toggle */ + z-index: 1; /* Place the icon above the switch */ +} +.sr-only { + border: 0 !important; + clip: rect(1px, 1px, 1px, 1px) !important; + clip-path: inset(50%) !important; + height: 1px !important; + margin: -1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + white-space: nowrap !important; + width: 1px !important; + + &-focusable { + &:focus, + &:active { + clip: auto !important; + clip-path: none !important; + height: auto !important; + margin: auto !important; + overflow: visible !important; + white-space: normal !important; + width: auto !important; + } + } +} + +// added some code for slightly prettier scrollbars + +::-webkit-scrollbar { + width: 20px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: #d6dee1; + border: 6px solid transparent; + border-radius: 20px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #a8bbbf; +} + +/* cui overrides */ + +.badge { + border-radius: var(--cipp-border-radius); +} + +.btn { + border-radius: var(--cipp-border-radius); + white-space: nowrap; +} + +.card { + border-radius: var(--cipp-border-radius); + + .card-header { + &:first-child { + background-clip: border-box; + border-radius: 3px 3px 0 0; + overflow: hidden; + } + } + + &.page-card { + border-radius: var(--cipp-border-radius); + + &:not(.datatable) { + border: 0; + } + + .card-header { + background: var(--cyberdrain-accent-green); + } + + .card-title { + font-size: 1rem; + line-height: 2rem; + margin-bottom: 0; + } + + > .card-body { + padding: 1rem 0; + } + + &.datatable { + .card-body { + padding: 1rem; + } + } + } + + &.options-card { + border-color: var(--cui-options-card-border-color); + + .card-header { + background: var(--cyberdrain-accent-blue); + border-bottom-color: var(--cui-options-card-border-color); + } + + .card-title { + line-height: 2rem; + margin-bottom: 0; + } + } + + &.content-card { + border-color: var(--cui-options-card-border-color); + + .card-header { + background: var(--cyberdrain-accent-blue); + // background: var(--cyberdrain-accent-blue); + border-bottom-color: var(--cui-options-card-border-color); + } + + .card-title { + line-height: 2rem; + margin-bottom: 0; + } + + .card-header-link { + color: var(--cui-headings-color); + font-weight: 500; + text-decoration: none; + + &:hover { + color: var(--cyberdrain-light-striped); + cursor: pointer; + text-decoration: underline; + } + } + } +} + +.dropdown-menu { + border-radius: var(--cipp-border-radius); +} + +.form-control { + border-radius: var(--cipp-border-radius); +} + +.form-select { + border-radius: var(--cipp-border-radius); + + /* stylelint-disable-next-line selector-class-pattern */ + .select-search__input { + border: 0; + } +} + +.modal { + z-index: 1555 !important; +} + +.modal-content { + border-radius: var(--cipp-border-radius); +} + +/* CIPP specifics */ + +.equalheight { + flex-grow: 1; +} + +.nav-pills { + .nav-link { + border-radius: var(--cipp-border-radius); + } +} + +.nav-tabs { + .nav-link { + border-radius: var(--cipp-border-radius); + } +} + +.progress { + border-radius: var(--cipp-border-radius); +} + +/* Tables */ + +/* stylelint-disable-next-line selector-class-pattern */ +.rdt_TableBody { + /* stylelint-disable-next-line selector-class-pattern */ + .rdt_TableRow { + &:hover { + border-bottom-color: var(--cipp-table-divider); + outline-style: none; + } + } + + /* stylelint-disable-next-line selector-class-pattern */ + .rdt_TableCell { + position: relative; + } +} + +.dropdown-item { + a { + color: var(--cui-dropdown-link-color); + display: inline-block; + text-decoration: none; + width: 100%; + + &:hover { + color: var(--cui-dropdown-link-hover-color); + text-decoration: none; + } + } +} +.offcanvas-large { + width: 800px !important; +} + +.cipp-offcanvas { + background-color: var(--cui-body-bg); + z-index: 1500; + + &.offcanvas-end { + @include ltr-rtl-value-only('transform', none, none); + } + + .offcanvas-header { + background-color: var(--cipp-offcanvas-header-bg); + color: var(--cipp-offcanvas-header-color); + padding: 0rem 1rem; + .btn { + color: var(--cipp-offcanvas-header-color); + + :hover { + color: var(--cipp-offcanvas-header-hover-color); + } + } + } + + .offcanvas-title { + background-color: var(--cipp-offcanvas-title-bg); + color: var(--cipp-offcanvas-title-color); + padding-bottom: 1rem; + padding-top: 1rem; + } + + .cipp-extendedinfo-label { + border-bottom: 1px solid var(--cipp-offcanvas-label-border-color); + font-weight: bold; + } +} + +.list-group { + border-radius: var(--cipp-border-radius); +} + +/* CIPP Text */ + +.bg-primary { + background-color: var(--cui-primary) !important; +} + +.bg-secondary { + background-color: var(--cui-secondary) !important; +} + +.bg-success { + background-color: var(--cui-success) !important; +} + +.bg-info { + background-color: var(--cui-info) !important; +} + +.bg-warning { + background-color: var(--cui-warning) !important; +} + +.bg-danger { + background-color: var(--cui-danger) !important; +} + +.text-primary { + color: var(--cui-primary) !important; +} + +.text-secondary { + color: var(--cui-secondary) !important; +} + +.text-success { + color: var(--cui-success) !important; +} + +.text-info { + color: var(--cui-info) !important; +} + +.text-warning { + color: var(--cui-warning) !important; +} + +.text-danger { + color: var(--cui-danger) !important; +} + +/* Code Block */ +.cipp-code { + position: relative; + + .cipp-code-copy-button { + position: absolute; + right: 0.1rem; + top: 0.1rem; + } +} + +.btn-as-block { + display: block; +} + +.fa-inverse { + color: var(--cipp-fa-inverse-color); +} + +.rdt_TableRow.no-mfa { + background-color: color.adjust($cyberdrain-danger, $alpha: -0.5) !important; + + .text-success { + color: var(--cui-body-color) !important; + } + + .text-danger { + color: var(--cui-body-color) !important; + } +} + +.rdt_TableRow.mbusage-warning { + background-color: color.adjust($cyberdrain-warning, $alpha: -0.5) !important; + + .text-success { + color: var(--cui-body-color) !important; + } + + .text-danger { + color: var(--cui-body-color) !important; + } +} + +.rdt_TableRow.mbusage-danger { + background-color: color.adjust($cyberdrain-danger, $alpha: -0.5) !important; + + .text-success { + color: var(--cui-body-color) !important; + } + + .text-danger { + color: var(--cui-body-color) !important; + } +} + +/* Footer */ + +.footer { + p { + margin-bottom: 0; + } + background: var(--cui-body-bg); + img { + max-height: 2rem; + } +} + +/* Toasts */ + +.toast-body { + .btn { + margin-left: 1rem; + } +} + +/* Sidebar */ + +.sidebar-nav .nav-link { + margin: 0.25rem 0; +} + +.react-select-container { + .react-select__control { + background-color: var(--cipp-search-bg); + border-color: var(--cipp-search-border-color); + transition: none; + + &:hover { + border-color: var(--cipp-search-border-color); + } + } + + .react-select__menu { + background-color: var(--cipp-search-bg); + border: 1px solid var(--cipp-search-border-color); + z-index: 1000; + } + + .react-select__option { + background-color: var(--cipp-search-bg); + + &:hover { + background-color: var(--cipp-search-border-color); + } + } + + .react-select__indicator { + color: var(--cipp-search-border-color); + } + + .react-select__indicator-separator { + background-color: var(--cipp-search-border-color); + } + + .react-select__placeholder, + .react-select__single-value { + color: var(--text-primary); + } +} + +.sidebar { + background-color: var(--cui-color-gray) !important; + svg { + color: var(--cui-color-black) !important; + transform: scale(0.8); + } + li { + color: var(--cui-color-black) !important; + a { + color: var(--cui-color-black) !important; + } + } + .simplebar-content > li:hover { + background: var(--cui-color-gray-hover) !important; + } + .nav-group.show { + background: var(--cui-color-gray-hover) !important; + } +} + +.wrapper.d-flex.flex-column.min-vh-100 { + //full width -20px for scrollbar bouncing + background-color: var(--cui-color-gray-1) !important; + z-index: 2; + // .body.flex-grow-1.px-xl-3>.container-fluid>div>div{ + // background: var(--cui-color-white) !important; + // padding: 20px 30px; + // margin-bottom: 20px; + // .nav-item> .nav-link { + // padding: 30px 20px; + // } + // } + .wrapper.d-flex.flex-column.min-vh-100 .card .card-body .cipp-tablewrapper { + padding: 0; + } + .card { + border: none; + margin-bottom: 20px; + box-shadow: 0 6px 10px 0 var(--cui-color-card-shadow), 0 8px 12px 0 var(--cui-color-card-shadow); + &:hover { + box-shadow: 0 8px 10px 0 var(--cui-color-card-shadow), + 0 8px 20px 0 var(--cui-color-card-shadow); + } + .card-header { + background-color: var(--cui-color-white); + color: var(--cui-color-black); + border: none; + } + .card-header.d-flex.justify-content-between { + padding: 10px 30px; + } + .card-body { + padding: 10px 40px; + padding-top: 0; + background-color: var(--cui-color-white); + .cipp-tablewrapper { + padding: 0; + } + header { + background-color: var(--cui-color-white) !important; + } + .sc-dlVxhl.fSQziN { + border-radius: 0px; + border: 1px solid var(--cui-color-gray-hover); + } + .rdt_TableHeadRow { + background-color: var(--cui-bgcolor-table-header) !important; + font-size: 12px; + color: var(--cui-color-white); + border-bottom-color: (--cui-color-gray-hover) !important; + padding: 8px 0; + } + .rdt_TableBody .rdt_TableRow:nth-child(odd) { + background-color: var(--cui-color-white) !important; + border-bottom-color: (--cui-color-gray-hover) !important; + } + .rdt_TableBody .rdt_TableRow:nth-child(even) { + background-color: var(--cui-color-gray) !important; + } + .rdt_Pagination { + select { + background-color: var(--cui-bgcolor-table-header) !important; + color: var(--cui-color-white) !important; + border-radius: 4px; + } + } + } + .sc-kfPuZi.iZiYoO { + background-color: var(--cui-color-white) !important; + } + } +} +.nav-item > .nav-link.active { + background-color: var(--cui-color-orange) !important; + color: var(--cui-color-white) !important; +} + +.btn.btn-primary { + color: var(--cui-color-white) !important; + border-radius: 4px; + border: none; +} +.dropdown-menu.show.tenantDropdown { + padding: 1px; +} + +.header.header-sticky { + background-color: var(--cui-color-gray) !important; + border-bottom: 2px solid var(--cui-color-gray-hover); +} +.header-toggler:hover { + transform: scale(1.1); +} + +.sidebar-brand { + background: none; + .sidebar-brand-full { + margin: 15px; + transform: scale(1.5); + } +} +.wrapper.d-flex.flex-column.min-vh-100 .card { + margin-bottom: 0px; +} + +.celltip-content-nowrap { + display: inline-block; + white-space: nowrap; + overflow: hidden; +} +.celltip-content { + display: inline-block; + white-space: normal; + overflow: ellipsis; +} + +.select-width { + min-width: 100%; +} + +.mr-15 { + margin-right: 15px; +} + +.no-colour { + .text-success { + color: var(--cui-body-color) !important; + } + + .text-danger { + color: var(--cui-body-color) !important; + } +} + +.teams-wide-card { + width: 500px; +} + +i.glyphicon { + display: none; +} + +.btn-add::after { + content: 'Add'; +} + +.array-item-copy::after { + content: 'Copy'; +} + +.array-item-move-up::after { + content: 'Move Up'; +} + +.array-item-move-down::after { + content: 'Move Down'; +} + +.array-item-remove::after { + content: 'Remove'; +} diff --git a/src/store/api/app.js b/src/store/api/app.js new file mode 100644 index 000000000000..bb2e4bb45ce2 --- /dev/null +++ b/src/store/api/app.js @@ -0,0 +1,143 @@ +import { baseApi } from 'src/store/api/baseApi' + +export const appApi = baseApi.injectEndpoints({ + endpoints: (builder) => ({ + loadVersions: builder.query({ + queryFn: (_args, _baseQueryApi, _options, baseQuery) => + baseQuery({ path: '/version_latest.txt' }).then(({ data }) => + baseQuery({ + path: '/api/GetVersion', + params: { localversion: data.replace(/(\r\n|\n|\r)/gm, '') }, + }), + ), + }), + loadVersionLocal: builder.query({ + query: () => ({ path: '/version_latest.txt' }), + }), + loadVersionRemote: builder.query({ + query: (localVersion) => ({ + path: '/api/GetVersion', + params: { localversion: localVersion }, + }), + }), + loadAlertsDash: builder.query({ + queryFn: (_args, _baseQueryApi, _options, baseQuery) => + baseQuery({ path: '/version_latest.txt' }).then(({ data }) => + baseQuery({ + path: '/api/GetCippAlerts', + params: { localversion: data.replace(/(\r\n|\n|\r)/gm, '') }, + }), + ), + }), + loadDash: builder.query({ + query: (localVersion) => ({ + path: '/api/GetDashboard', + }), + }), + execPermissionsAccessCheck: builder.query({ + query: () => ({ + path: '/api/ExecAccessChecks', + params: { + Permissions: true, + }, + }), + }), + execNotificationConfig: builder.query({ + query: ({ + email, + webhook, + tokenUpdater, + removeUser, + removeStandard, + addPolicy, + addUser, + addStandardsDeploy, + addChocoApp, + onePerTenant, + sendtoIntegration, + includeTenantId, + logsToInclude, + Severity, + }) => ({ + path: '/api/ExecNotificationConfig', + data: { + email: email, + webhook: webhook, + tokenUpdater: tokenUpdater, + removeUser: removeUser, + removeStandard: removeStandard, + addPolicy: addPolicy, + addUser: addUser, + addStandardsDeploy: addStandardsDeploy, + addChocoApp: addChocoApp, + onePerTenant: onePerTenant, + logsToInclude: logsToInclude, + Severity: Severity, + sendtoIntegration: sendtoIntegration, + includeTenantId: includeTenantId, + }, + method: 'post', + }), + }), + execTenantsAccessCheck: builder.query({ + query: ({ tenantDomains }) => ({ + path: '/api/ExecAccessChecks', + params: { + Tenants: true, + }, + data: { + tenantid: tenantDomains.join(','), + }, + method: 'post', + }), + }), + execClearCache: builder.query({ + query: ({ tenantsOnly }) => ({ + path: '/api/ListTenants', + params: { + ClearCache: true, + TenantsOnly: tenantsOnly, + }, + }), + }), + listNotificationConfig: builder.query({ + query: () => ({ + path: '/api/listNotificationConfig', + }), + }), + genericPostRequest: builder.query({ + query: ({ path, values }) => ({ + path, + data: values, + method: 'post', + }), + }), + genericGetRequest: builder.query({ + query: ({ path, params }) => ({ + path, + params: params, + method: 'GET', + }), + }), + }), +}) + +export const { + useLoadVersionLocalQuery, + useLoadVersionRemoteQuery, + useLoadVersionsQuery, + useLoadDashQuery, + useLoadAlertsDashQuery, + useExecPermissionsAccessCheckQuery, + useLazyExecPermissionsAccessCheckQuery, + useExecTenantsAccessCheckQuery, + useLazyExecTenantsAccessCheckQuery, + useExecClearCacheQuery, + useLazyExecClearCacheQuery, + useLazyExecNotificationConfigQuery, + useLazyListNotificationConfigQuery, + useLazyGenericPostRequestQuery, + useLazyGenericGetRequestQuery, + useGenericGetRequestQuery, + useGenericPostRequestQuery, +} = appApi diff --git a/src/store/api/reports.js b/src/store/api/reports.js new file mode 100644 index 000000000000..2292662ad2be --- /dev/null +++ b/src/store/api/reports.js @@ -0,0 +1,21 @@ +import { baseApi } from 'src/store/api/baseApi' + +export const reportsApi = baseApi.injectEndpoints({ + endpoints: (builder) => ({ + listBestPracticeAnalyser: builder.query({ + query: () => ({ path: '/api/BestPracticeAnalyser_List' }), + }), + execBestPracticeAnalyser: builder.mutation({ + query: () => ({ path: '/api/ExecBPA' }), + }), + execDomainsAnalyser: builder.mutation({ + query: () => ({ path: '/api/ExecDomainAnalyser' }), + }), + }), +}) + +export const { + useListBestPracticeAnalyserQuery, + useExecBestPracticeAnalyserMutation, + useExecDomainsAnalyserMutation, +} = reportsApi diff --git a/src/store/api/users.js b/src/store/api/users.js new file mode 100644 index 000000000000..552481567406 --- /dev/null +++ b/src/store/api/users.js @@ -0,0 +1,132 @@ +import { baseApi } from 'src/store/api/baseApi' + +export const usersApi = baseApi.injectEndpoints({ + endpoints: (builder) => ({ + editUser: builder.mutation({ + query: (user) => ({ + path: '/api/EditUser', + method: 'post', + data: user, + }), + }), + listUsers: builder.query({ + query: ({ tenantDomain }) => ({ + path: '/api/ListUsers', + params: { + TenantFilter: tenantDomain, + }, + }), + }), + listContacts: builder.query({ + query: ({ tenantDomain, ContactID }) => ({ + path: '/api/ListContacts', + params: { + TenantFilter: tenantDomain, + ContactID, + }, + }), + }), + listUser: builder.query({ + query: ({ tenantDomain, userId, IncludeLogonDetails }) => ({ + path: '/api/ListUsers', + params: { userId, TenantFilter: tenantDomain, IncludeLogonDetails }, + }), + transformResponse: (response) => { + if (response?.length > 0) { + return response[0] + } + return {} + }, + }), + listUserConditionalAccessPolicies: builder.query({ + query: ({ tenantDomain, userId }) => ({ + path: '/api/ListUserConditionalAccessPolicies', + params: { userId, tenantFilter: tenantDomain }, + }), + }), + listUserSigninLogs: builder.query({ + query: ({ tenantDomain, userId }) => ({ + path: '/api/ListUserSigninLogs', + params: { userId, tenantFilter: tenantDomain }, + }), + }), + addUser: builder.mutation({ + query: ({ user }) => ({ + path: '/api/AddUser', + data: user, + }), + }), + execBecCheck: builder.query({ + queryFn: async (_args, _baseQueryApi, _options, baseQuery) => { + const startRequest = await baseQuery({ + path: '/api/execBECCheck', + params: { + userId: _args.userId, + tenantFilter: _args.tenantFilter, + userName: _args.userName, + overwrite: _args.overwrite, + }, + }) + if (startRequest.error) { + return { error: startRequest.error } + } + + const GUID = startRequest.data?.GUID + + return new Promise((resolve) => { + let retries = 0 + const interval = setInterval(async () => { + const { data, error } = await baseQuery({ + path: '/api/execBECCheck', + params: { GUID }, + }) + if (error) { + clearInterval(interval) + resolve({ error }) + } + if (data.Results) { + clearInterval(interval) + resolve({ + error: { + message: `Error: ${data.Results}`, + status: 503, + data: 'Request failed.', + }, + }) + } + if (!data['Waiting'] === true) { + if (!Array.isArray(data['MSResults'])) { + data['MSResults'] = [] + } + clearInterval(interval) + resolve({ data }) + } + if (retries >= 60) { + clearInterval(interval) + resolve({ + error: { + message: 'Failed to retrieve data in 5 minutes', + status: 503, + data: 'Request failed with status 503', + }, + }) + } + retries++ + }, 5000) + }) + }, + }), + }), +}) + +export const { + useEditUserMutation, + useListUsersQuery, + useListUserQuery, + useListContactsQuery, + useListUserConditionalAccessPoliciesQuery, + useListUserSigninLogsQuery, + useAddUserMutation, + useLazyExecBecCheckQuery, +} = usersApi +export default usersApi diff --git a/src/views/cipp/AppApprovalTemplates.jsx b/src/views/cipp/AppApprovalTemplates.jsx new file mode 100644 index 000000000000..6f9159565dcd --- /dev/null +++ b/src/views/cipp/AppApprovalTemplates.jsx @@ -0,0 +1,140 @@ +import React from 'react' +import { + CCol, + CRow, + CCallout, + CSpinner, + CButton, + CFormInput, + CFormLabel, + CTooltip, +} from '@coreui/react' +import { Field, Form, FormSpy } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' +import { CippPageList, CippWizard } from 'src/components/layout' +import { cellDateFormatter, CippTable, WizardTableField } from 'src/components/tables' +import PropTypes from 'prop-types' +import { + Condition, + RFFCFormCheck, + RFFCFormInput, + RFFCFormSwitch, + RFFSelectSearch, +} from 'src/components/forms' +import { useLazyGenericPostRequestQuery } from 'src/store/api/app' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { CippOffcanvas } from 'src/components/utilities' +import CippAppPermissionBuilder from 'src/components/utilities/CippAppPermissionBuilder' + +const AppApprovalTemplates = () => { + const [editorVisible, setEditorVisible] = React.useState(false) + const [selectedTemplate, setSelectedTemplate] = React.useState(null) + const templateNameRef = React.useRef(null) + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const onSubmit = (values) => { + var body = { + TemplateName: templateNameRef.current.value, + Permissions: values.Permissions, + } + if (selectedTemplate?.TemplateId) { + body.TemplateId = selectedTemplate.TemplateId + } + + console.log(body) + genericPostRequest({ + path: '/api/ExecAppPermissionTemplate?Action=Save', + values: body, + }).then(() => {}) + } + const titleButton = ( + { + setSelectedTemplate({}) + templateNameRef.current.value = '' + setEditorVisible(true) + }} + > + Add Template + + ) + return ( + <> + row['TemplateName'], + sortable: true, + exportSelector: 'TemplateName', + }, + { + name: 'Updated By', + selector: (row) => row['UpdatedBy'], + sortable: true, + exportSelector: 'UpdatedBy', + }, + { + name: 'Updated At', + selector: (row) => row['Timestamp'], + sortable: true, + exportSelector: 'Timestamp', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'Actions', + cell: (row) => ( + + { + setSelectedTemplate(row) + templateNameRef.current.value = row.TemplateName + setEditorVisible(true) + }} + > + + + + ), + }, + ], + reportName: 'AppApprovalTemplates', + }} + /> + setEditorVisible(false)} + > + Template Name + + Permissions + + + + ) +} + +export default AppApprovalTemplates diff --git a/src/views/cipp/app-settings/SettingsExtensionMappings.jsx b/src/views/cipp/app-settings/SettingsExtensionMappings.jsx new file mode 100644 index 000000000000..4ed3362e2b59 --- /dev/null +++ b/src/views/cipp/app-settings/SettingsExtensionMappings.jsx @@ -0,0 +1,651 @@ +import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js' +import { + CAccordion, + CButton, + CCallout, + CCardText, + CCol, + CForm, + CRow, + CSpinner, + CTooltip, +} from '@coreui/react' +import { Form } from 'react-final-form' +import { RFFSelectSearch } from 'src/components/forms/index.js' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import React, { useEffect } from 'react' +import { CippCallout } from 'src/components/layout/index.js' +import CippAccordionItem from 'src/components/contentcards/CippAccordionItem' +import { CippTable } from 'src/components/tables' +import { CellTip } from 'src/components/tables/CellGenericFormat' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' + +/** + * Retrieves and sets the extension mappings for HaloPSA and NinjaOne. + * + * @returns {JSX.Element} - JSX component representing the settings extension mappings. + */ +export function SettingsExtensionMappings({ type }) { + const [addedAttributes, setAddedAttribute] = React.useState(1) + const [mappingArray, setMappingArray] = React.useState('defaultMapping') + const [mappingValue, setMappingValue] = React.useState({}) + const [haloMappingsArray, setHaloMappingsArray] = React.useState([]) + const [ninjaMappingsArray, setNinjaMappingsArray] = React.useState([]) + const [HaloAutoMap, setHaloAutoMap] = React.useState(false) + const [listHaloBackend, listBackendHaloResult = []] = useLazyGenericGetRequestQuery() + const [listNinjaOrgsBackend, listBackendNinjaOrgsResult] = useLazyGenericGetRequestQuery() + const [listNinjaFieldsBackend, listBackendNinjaFieldsResult] = useLazyGenericGetRequestQuery() + const [setHaloExtensionconfig, extensionHaloConfigResult = []] = useLazyGenericPostRequestQuery() + const [setNinjaOrgsExtensionconfig, extensionNinjaOrgsConfigResult] = + useLazyGenericPostRequestQuery() + const [setNinjaOrgsExtensionAutomap, extensionNinjaOrgsAutomapResult] = + useLazyGenericPostRequestQuery() + const [setNinjaFieldsExtensionconfig, extensionNinjaFieldsConfigResult] = + useLazyGenericPostRequestQuery() + + const onHaloSubmit = () => { + const originalFormat = haloMappingsArray.reduce((acc, item) => { + acc[item.Tenant?.customerId] = { label: item.haloName, value: item.haloId } + return acc + }, {}) + setHaloExtensionconfig({ + path: 'api/ExecExtensionMapping?AddMapping=Halo', + values: { mappings: originalFormat }, + }).then(() => { + listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' }) + setMappingValue({}) + }) + } + const onNinjaOrgsSubmit = () => { + const originalFormat = ninjaMappingsArray.reduce((acc, item) => { + acc[item.Tenant?.customerId] = { label: item.ninjaName, value: item.ninjaId } + return acc + }, {}) + + setNinjaOrgsExtensionconfig({ + path: 'api/ExecExtensionMapping?AddMapping=NinjaOrgs', + values: { mappings: originalFormat }, + }).then(() => { + listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' }) + setMappingValue({}) + }) + } + + const onNinjaOrgsAutomap = async (values) => { + await setNinjaOrgsExtensionAutomap({ + path: 'api/ExecExtensionMapping?AutoMapping=NinjaOrgs', + values: { mappings: values }, + }) + await listNinjaOrgsBackend({ + path: 'api/ExecExtensionMapping?List=NinjaOrgs', + }) + } + + const onNinjaFieldsSubmit = (values) => { + setNinjaFieldsExtensionconfig({ + path: 'api/ExecExtensionMapping?AddMapping=NinjaFields', + values: { mappings: values }, + }) + } + + const onHaloAutomap = () => { + const newMappings = listBackendHaloResult.data?.Tenants.map( + (tenant) => { + const haloClient = listBackendHaloResult.data?.HaloClients.find( + (client) => client.name === tenant.displayName, + ) + if (haloClient) { + console.log(haloClient) + console.log(tenant) + return { + Tenant: tenant, + haloName: haloClient.name, + haloId: haloClient.value, + } + } + }, + //filter out any undefined values + ).filter((item) => item !== undefined) + setHaloMappingsArray((currentHaloMappings) => [...currentHaloMappings, ...newMappings]).then( + () => { + listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' }) + }, + ) + setHaloAutoMap(true) + } + + useEffect(() => { + if (listBackendHaloResult.isSuccess) { + setHaloMappingsArray( + Object.keys(listBackendHaloResult.data?.Mappings).map((key) => ({ + Tenant: listBackendHaloResult.data?.Tenants.find((tenant) => tenant.customerId === key), + haloName: listBackendHaloResult.data?.Mappings[key].label, + haloId: listBackendHaloResult.data?.Mappings[key].value, + })), + ) + } + }, [listBackendHaloResult.isSuccess]) + + useEffect(() => { + if (listBackendNinjaOrgsResult.isSuccess) { + setNinjaMappingsArray( + Object.keys(listBackendNinjaOrgsResult.data?.Mappings).map((key) => ({ + Tenant: listBackendNinjaOrgsResult.data?.Tenants.find( + (tenant) => tenant.customerId === key, + ), + ninjaName: listBackendNinjaOrgsResult.data?.Mappings[key].label, + ninjaId: listBackendNinjaOrgsResult.data?.Mappings[key].value, + })), + ) + } + }, [ + listBackendNinjaOrgsResult.data?.Mappings, + listBackendNinjaOrgsResult.data?.Tenants, + listBackendNinjaOrgsResult.isSuccess, + ]) + + const Offcanvas = (row, rowIndex, formatExtraData) => { + return ( + <> + + + row.haloId + ? setHaloMappingsArray((currentHaloMappings) => + currentHaloMappings.filter((item) => item !== row), + ) + : setNinjaMappingsArray((currentNinjaMappings) => + currentNinjaMappings.filter((item) => item !== row), + ) + } + > + + + + + ) + } + const haloColumns = [ + { + name: 'Tenant', + selector: (row) => row.Tenant?.displayName, + sortable: true, + cell: (row) => CellTip(row.Tenant?.displayName), + exportSelector: 'Tenant', + }, + { + name: 'TenantId', + selector: (row) => row.Tenant?.customerId, + sortable: true, + exportSelector: 'Tenant/customerId', + omit: true, + }, + { + name: 'Halo Client Name', + selector: (row) => row['haloName'], + sortable: true, + cell: (row) => CellTip(row['haloName']), + exportSelector: 'haloName', + }, + { + name: 'Halo ID', + selector: (row) => row['haloId'], + sortable: true, + cell: (row) => CellTip(row['haloId']), + exportSelector: 'haloId', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '80px', + }, + ] + + const ninjaColumns = [ + { + name: 'Tenant', + selector: (row) => row.Tenant?.displayName, + sortable: true, + cell: (row) => CellTip(row.Tenant?.displayName), + exportSelector: 'Tenant', + }, + { + name: 'TenantId', + selector: (row) => row.Tenant?.customerId, + sortable: true, + exportSelector: 'Tenant/customerId', + omit: true, + }, + { + name: 'NinjaOne Organization Name', + selector: (row) => row['ninjaName'], + sortable: true, + cell: (row) => CellTip(row['ninjaName']), + exportSelector: 'ninjaName', + }, + { + name: 'NinjaOne Organization ID', + selector: (row) => row['ninjaId'], + sortable: true, + cell: (row) => CellTip(row['ninjaId']), + exportSelector: 'ninjaId', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '80px', + }, + ] + + return ( + + {type === 'HaloPSA' && ( + <> + {listBackendHaloResult.isUninitialized && + listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' })} + + + + {extensionHaloConfigResult.isFetching && ( + + )} + Save Mappings + + onHaloAutomap()} className="me-2"> + {extensionNinjaOrgsAutomapResult.isFetching && ( + + )} + Automap HaloPSA Clients + + + } + > + {listBackendHaloResult.isFetching && listBackendHaloResult.isUninitialized ? ( + + ) : ( + { + return ( + + + Use the table below to map your client to the correct PSA client. + { + //load all the existing mappings and show them first in a table. + listBackendHaloResult.isSuccess && ( + + ) + } + + + { + return !Object.keys(listBackendHaloResult.data?.Mappings).includes( + tenant.customerId, + ) + }).map((tenant) => ({ + name: tenant.displayName, + value: tenant.customerId, + }))} + onChange={(e) => { + setMappingArray(e.value) + }} + isLoading={listBackendHaloResult.isFetching} + /> + + + + + + { + return !Object.values(listBackendHaloResult.data?.Mappings) + .map((value) => { + return value.value + }) + .includes(client.value) + }).map((client) => ({ + name: client.name, + value: client.value, + }))} + onChange={(e) => setMappingValue(e)} + placeholder="Select a HaloPSA Client" + isLoading={listBackendHaloResult.isFetching} + /> + + { + if ( + mappingValue.value !== undefined && + mappingValue.value !== '-1' && + Object.values(haloMappingsArray) + .map((item) => item.haloId) + .includes(mappingValue.value) === false + ) { + //set the new mapping in the array + setHaloMappingsArray([ + ...haloMappingsArray, + { + Tenant: listBackendHaloResult.data?.Tenants.find( + (tenant) => tenant.customerId === mappingArray, + ), + haloName: mappingValue.label, + haloId: mappingValue.value, + }, + ]) + } + }} + className={`my-4 circular-button`} + title={'+'} + > + + + + + + {HaloAutoMap && ( + + Automapping has been executed. Remember to check the changes and save + them. + + )} + {(extensionHaloConfigResult.isSuccess || + extensionHaloConfigResult.isError) && + !extensionHaloConfigResult.isFetching && ( + + {extensionHaloConfigResult.isSuccess + ? extensionHaloConfigResult.data.Results + : 'Error'} + + )} + + + + After editing the mappings you must click Save Mappings for the changes to + take effect. The table will be saved exactly as presented. + + + ) + }} + /> + )} + + + )} + {type === 'NinjaOne' && ( + <> + {listBackendNinjaOrgsResult.isUninitialized && + listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' })} + {listBackendNinjaFieldsResult.isUninitialized && + listNinjaFieldsBackend({ path: 'api/ExecExtensionMapping?List=NinjaFields' })} + + + {extensionNinjaOrgsConfigResult.isFetching && ( + + )} + Set Mappings + + onNinjaOrgsAutomap()} className="me-2"> + {extensionNinjaOrgsAutomapResult.isFetching && ( + + )} + Automap NinjaOne Organizations + + + } + > + {listBackendNinjaOrgsResult.isFetching && listBackendNinjaOrgsResult.isUninitialized ? ( + + ) : ( + { + return ( + + + Use the table below to map your client to the correct NinjaOne Organization. + { + //load all the existing mappings and show them first in a table. + listBackendNinjaOrgsResult.isSuccess && ( + + ) + } + + + { + return !Object.keys( + listBackendNinjaOrgsResult.data?.Mappings, + ).includes(tenant.customerId) + }).map((tenant) => ({ + name: tenant.displayName, + value: tenant.customerId, + }))} + onChange={(e) => { + setMappingArray(e.value) + }} + isLoading={listBackendNinjaOrgsResult.isFetching} + /> + + + + + + { + return !Object.values(listBackendNinjaOrgsResult.data?.Mappings) + .map((value) => { + return value.value + }) + .includes(client.value.toString()) + }, + ).map((client) => ({ + name: client.name, + value: client.value, + }))} + onChange={(e) => setMappingValue(e)} + placeholder="Select a NinjaOne Organization" + isLoading={listBackendNinjaOrgsResult.isFetching} + /> + + { + //set the new mapping in the array + if ( + mappingValue.value !== undefined && + mappingValue.value !== '-1' && + Object.values(ninjaMappingsArray) + .map((item) => item.ninjaId) + .includes(mappingValue.value) === false + ) { + setNinjaMappingsArray([ + ...ninjaMappingsArray, + { + Tenant: listBackendNinjaOrgsResult.data?.Tenants.find( + (tenant) => tenant.customerId === mappingArray, + ), + ninjaName: mappingValue.label, + ninjaId: mappingValue.value, + }, + ]) + } + }} + className={`my-4 circular-button`} + title={'+'} + > + + + + + + {(extensionNinjaOrgsAutomapResult.isSuccess || + extensionNinjaOrgsAutomapResult.isError) && + !extensionNinjaOrgsAutomapResult.isFetching && ( + + {extensionNinjaOrgsAutomapResult.isSuccess + ? extensionNinjaOrgsAutomapResult.data.Results + : 'Error'} + + )} + {(extensionNinjaOrgsConfigResult.isSuccess || + extensionNinjaOrgsConfigResult.isError) && + !extensionNinjaOrgsConfigResult.isFetching && ( + + {extensionNinjaOrgsConfigResult.isSuccess + ? extensionNinjaOrgsConfigResult.data.Results + : 'Error'} + + )} + + + + After editing the mappings you must click Save Mappings for the changes to + take effect. The table will be saved exactly as presented. + + + ) + }} + /> + )} + + + )} + {type === 'NinjaOne' && ( + + {extensionNinjaFieldsConfigResult.isFetching && ( + + )} + Set Mappings + + } + > + {listBackendNinjaFieldsResult.isFetching ? ( + + ) : ( + { + return ( + + +
Organization Global Custom Field Mapping
+

+ Use the table below to map your Organization Field to the correct NinjaOne + Field +

+ {listBackendNinjaFieldsResult.isSuccess && + listBackendNinjaFieldsResult.data.CIPPOrgFields.map((CIPPOrgFields) => ( + item.type === CIPPOrgFields.Type || item.type === 'unset', + )} + placeholder="Select a Field" + /> + ))} +
+ +
Device Custom Field Mapping
+

+ Use the table below to map your Device field to the correct NinjaOne WYSIWYG + Field +

+ {listBackendNinjaFieldsResult.isSuccess && + listBackendNinjaFieldsResult.data.CIPPNodeFields.map((CIPPNodeFields) => ( + item.type === CIPPNodeFields.Type || item.type === 'unset', + )} + placeholder="Select a Field" + /> + ))} +
+ + {(extensionNinjaFieldsConfigResult.isSuccess || + extensionNinjaFieldsConfigResult.isError) && + !extensionNinjaFieldsConfigResult.isFetching && ( + + {extensionNinjaFieldsConfigResult.isSuccess + ? extensionNinjaFieldsConfigResult.data.Results + : 'Error'} + + )} + +
+ ) + }} + /> + )} +
+ )} +
+ ) +} diff --git a/src/views/cipp/app-settings/components/SettingsAppPermissions.jsx b/src/views/cipp/app-settings/components/SettingsAppPermissions.jsx new file mode 100644 index 000000000000..55a56aea7f5e --- /dev/null +++ b/src/views/cipp/app-settings/components/SettingsAppPermissions.jsx @@ -0,0 +1,75 @@ +import React, { useRef, useState } from 'react' +import { + CButton, + CCallout, + CCol, + CForm, + CRow, + CAccordion, + CAccordionHeader, + CAccordionBody, + CAccordionItem, +} from '@coreui/react' +import { Field, Form, FormSpy } from 'react-final-form' +import { RFFCFormRadioList, RFFSelectSearch } from 'src/components/forms' +import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { TenantSelectorMultiple, ModalService, CippOffcanvas } from 'src/components/utilities' +import PropTypes from 'prop-types' +import { OnChange } from 'react-final-form-listeners' +import { useListTenantsQuery } from 'src/store/api/tenants' +import { OffcanvasListSection } from 'src/components/utilities/CippListOffcanvas' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' +import CippAppPermissionBuilder from 'src/components/utilities/CippAppPermissionBuilder' +import Skeleton from 'react-loading-skeleton' + +const SettingsAppPermissions = () => { + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const handleSubmit = (values) => { + genericPostRequest({ + path: 'api/ExecSAMAppPermissions?Action=Update', + values: values, + }).then(() => { + refetchSam() + }) + } + + const { + data: samAppPermissions = [], + isFetching: samAppPermissionsFetching, + isSuccess: samAppPermissionsSuccess, + refetch: refetchSam, + } = useGenericGetRequestQuery({ + path: 'api/ExecSAMAppPermissions', + }) + + return ( + + <> +

Manage the permissions for the CIPP-SAM App Registration and CPV.

+

+ This functionality is in + beta and should be treated as such. Removing permissions from the CIPP-SAM App is not + advised. +

+ + {samAppPermissionsFetching && } + {samAppPermissionsSuccess && ( + + )} + + {postResults.data && ( + + {postResults?.data?.Results} + + )} + +
+ ) +} + +export default SettingsAppPermissions diff --git a/src/views/email-exchange/administration/EditMailboxPermissions.jsx b/src/views/email-exchange/administration/EditMailboxPermissions.jsx new file mode 100644 index 000000000000..740aa2417455 --- /dev/null +++ b/src/views/email-exchange/administration/EditMailboxPermissions.jsx @@ -0,0 +1,1209 @@ +import React, { useEffect, useRef, useState } from 'react' +import { + CButton, + CCallout, + CCard, + CCardBody, + CCardHeader, + CCardTitle, + CCol, + CNav, + CNavItem, + CTabContent, + CTabPane, + CForm, + CRow, + CSpinner, + CLink, + CBadge, +} from '@coreui/react' +import useQuery from 'src/hooks/useQuery' +import { useDispatch } from 'react-redux' +import { Form, Field } from 'react-final-form' +import { RFFSelectSearch, RFFCFormCheck, RFFCFormInput, RFFCFormSwitch } from 'src/components/forms' +import { CippLazy, ModalService } from 'src/components/utilities' +import { + useLazyGenericPostRequestQuery, + useLazyGenericGetRequestQuery, + useGenericGetRequestQuery, + useGenericPostRequestQuery, +} from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { useListMailboxDetailsQuery, useListMailboxPermissionsQuery } from 'src/store/api/mailbox' +import { CellBadge, CellBoolean, CippDatatable } from 'src/components/tables' +import DatePicker from 'react-datepicker' +import 'react-datepicker/dist/react-datepicker.css' +import PropTypes from 'prop-types' +import Skeleton from 'react-loading-skeleton' + +const formatter = (cell, warning = false, reverse = false, colourless = false) => + CellBoolean({ cell, warning, reverse, colourless }) + +const MailboxSettings = () => { + const dispatch = useDispatch() + let query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + const [active, setActive] = useState(1) + const [forwardingRefresh, setForwardingRefresh] = useState('0') + const [oooRefresh, setOooRefresh] = useState('0') + const columnsCal = [ + { + name: 'User', + selector: (row) => row['User'], + sortable: true, + wrap: true, + cell: (row) => row['User'], + exportSelector: 'User', + maxWidth: '150px', + }, + { + name: 'AccessRights', + selector: (row) => row['AccessRights'], + sortable: true, + wrap: true, + cell: (row) => row['AccessRights'], + exportSelector: 'AccessRights', + maxWidth: '150px', + }, + { + name: 'Identity', + selector: (row) => row['Identity'], + sortable: true, + wrap: true, + cell: (row) => row['Identity'], + exportSelector: 'Identity', + maxWidth: '150px', + }, + ] + const columns = [ + { + name: 'User', + selector: (row) => row.User, + sortable: true, + wrap: true, + exportSelector: 'User', + }, + { + name: 'Permissions', + selector: (row) => row['Permissions'], + sortable: true, + wrap: true, + exportSelector: 'Permissions', + }, + ] + + return ( + + + + + + setActive(1)} href="#"> + Mailbox Permissions + + setActive(2)} href="#"> + Calendar Permissions + + setActive(3)} href="#"> + Mailbox Forwarding + + setActive(4)} href="#"> + Out Of Office + + + + + + + + + + + + + + + + + + + setForwardingRefresh((Math.random() + 1).toString(36).substring(7)) + } + /> + + + + + + setOooRefresh((Math.random() + 1).toString(36).substring(7)) + } + /> + + + + + + + + + + Account Information - {userId} + + + {active === 1 && ( + + )} + {active === 2 && ( + + )} + {active === 3 && ( + <> + + + )} + {active === 4 && ( + <> + + + )} + + + + + ) +} + +export default MailboxSettings + +const MailboxPermissions = () => { + const dispatch = useDispatch() + let query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + + const [queryError, setQueryError] = useState(false) + + //const [EditMailboxPermission, { error: EditMailboxPermissionError, isFetching: EditMailboxPermissionIsFetching }] = useEditMailboxPermissionMutation() + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const { + data: users = [], + isFetching: usersIsFetching, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + Endpoint: 'users', + TenantFilter: tenantDomain, + $filter: "assignedLicenses/$count ne 0 and accountEnabled eq true and userType eq 'Member'", + $select: 'id,displayName,userPrincipalName', + $count: true, + $orderby: 'displayName', + }, + }) + + useEffect(() => { + if (!userId || !tenantDomain) { + ModalService.open({ + body: 'Error invalid request, could not load requested user.', + title: 'Invalid Request', + }) + setQueryError(true) + } else { + setQueryError(false) + } + }, [userId, tenantDomain, dispatch, postResults]) + const onSubmit = (values) => { + const shippedValues = { + userid: userId, + tenantFilter: tenantDomain, + AddFullAccessNoAutoMap: values.AddFullAccessNoAutoMap ? values.AddFullAccessNoAutoMap : null, + AddFullAccess: values.AddFullAccess ? values.AddFullAccess : null, + RemoveFullAccess: values.RemoveFullAccess ? values.RemoveFullAccess : null, + AddSendAs: values.AddSendAs ? values.AddSendAs : null, + RemoveSendAs: values.RemoveSendAs ? values.RemoveSendAs : null, + AddSendOnBehalf: values.AddSendOnBehalf ? values.AddSendOnBehalf : null, + RemoveSendOnBehalf: values.RemoveSendOnBehalf ? values.RemoveSendOnBehalf : null, + } + //window.alert(JSON.stringify(shippedValues)) + genericPostRequest({ path: '/api/ExecEditMailboxPermissions', values: shippedValues }) + } + + const formDisabled = queryError === true + + return ( + <> + {!queryError && ( + <> + {queryError && ( + + + + {/* @todo add more descriptive help message here */} + Failed to load user + + + + )} + + {usersIsFetching && } + {!usersIsFetching && ( + { + return ( + + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="RemoveFullAccess" + /> + {usersError && Failed to load list of users} + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="AddFullAccess" + /> + {usersError && Failed to load list of users} + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="AddFullAccessNoAutoMap" + /> + {usersError && Failed to load list of users} + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="AddSendAs" + /> + {usersError && Failed to load list of users} + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="RemoveSendAs" + /> + {usersError && Failed to load list of users} + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="AddSendOnBehalf" + /> + {usersError && Failed to load list of users} + + + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="RemoveSendOnBehalf" + /> + {usersError && Failed to load list of users} + + + + + + Edit User Permissions + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + + {postResults.data.Results.map((result, idx) => ( +
  • {result}
  • + ))} +
    + )} +
    + ) + }} + /> + )} +
    + + )} + + ) +} + +const CalendarPermissions = () => { + const dispatch = useDispatch() + let query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + + const [queryError, setQueryError] = useState(false) + + const { + data: user = [], + isFetching: userIsFetching, + error: userError, + } = useGenericGetRequestQuery({ + path: '/api/ListCalendarPermissions', + params: { TenantFilter: tenantDomain, UserId: userId }, + }) + + const { + data: users = [], + isFetching: usersIsFetching, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListMailboxes', + params: { TenantFilter: tenantDomain, SkipLicense: true }, + }) + + useEffect(() => { + if (!userId || !tenantDomain) { + ModalService.open({ + body: 'Error invalid request, could not load requested user.', + title: 'Invalid Request', + }) + setQueryError(true) + } else { + setQueryError(false) + } + }, [userId, tenantDomain, dispatch]) + const [genericPostRequest, postResults] = useLazyGenericGetRequestQuery() + const onSubmit = (values) => { + const shippedValues = { + FolderName: user[0].FolderName, + userid: userId, + tenantFilter: tenantDomain, + Permissions: values.Permissions ? values.Permissions.value : '', + UserToGetPermissions: values.UserToGetPermissions ? values.UserToGetPermissions.value : '', + RemoveAccess: values.RemoveAccess ? values.RemoveAccess.value : '', + } + //window.alert(JSON.stringify(shippedValues)) + genericPostRequest({ path: '/api/ExecEditCalendarPermissions', params: shippedValues }) + } + const initialState = {} + + // this is dumb + const formDisabled = queryError === true + + const UsersMapped = users?.map((user) => ({ + value: `${user.primarySmtpAddress}`, + name: `${user.displayName} - (${user.primarySmtpAddress})`, + })) + UsersMapped.unshift({ value: 'Default', name: 'Default' }) + + return ( + <> + {queryError && ( + + + + {/* @todo add more descriptive help message here */} + Failed to load user + + + + )} + {!queryError && ( + <> + + + {userIsFetching && } + {userError && Error loading user} + {!userIsFetching && ( + { + return ( + + + + + {usersError && Failed to load list of users} + + + + {usersError && Failed to load list of users} + + + + {usersError && Failed to load list of users} + + + + + + Edit Permissions + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + {postResults.data?.Results} + )} + + ) + }} + /> + )} + + + + )} + + ) +} + +const MailboxForwarding = ({ refreshFunction }) => { + const dispatch = useDispatch() + let query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + + const [queryError, setQueryError] = useState(false) + + //const [EditMailboxPermission, { error: EditMailboxPermissionError, isFetching: EditMailboxPermissionIsFetching }] = useEditMailboxPermissionMutation() + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const { + data: user = {}, + isFetching: userIsFetching, + error: userError, + } = useListMailboxPermissionsQuery({ tenantDomain, userId }) + + const { + data: users = [], + isFetching: usersIsFetching, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + Endpoint: 'users', + TenantFilter: tenantDomain, + $filter: "userType eq 'Member' and proxyAddresses/$count ne 0", + $select: 'id,displayName,userPrincipalName', + $count: true, + $orderby: 'displayName', + }, + }) + useEffect(() => { + if (postResults.isSuccess) { + // @TODO do something here? + } + if (!userId || !tenantDomain) { + ModalService.open({ + body: 'Error invalid request, could not load requested user.', + title: 'Invalid Request', + }) + setQueryError(true) + } else { + setQueryError(false) + } + }, [userId, tenantDomain, dispatch, postResults]) + const onSubmit = (values) => { + const shippedValues = { + userid: userId, + tenantFilter: tenantDomain, + ForwardInternal: values.ForwardInternal ? values.ForwardInternal : '', + ForwardExternal: values.ForwardExternal ? values.ForwardExternal : '', + KeepCopy: values.KeepCopy ? true : false, + disableForwarding: values.forwardOption === 'disabled', + } + //window.alert(JSON.stringify(shippedValues)) + genericPostRequest({ path: '/api/ExecEmailForward', values: shippedValues }).then(() => { + refreshFunction() + }) + } + const initialState = { + ...user, + } + + const formDisabled = queryError === true + + return ( + <> + {!queryError && ( + <> + {queryError && ( + + + + {/* @todo add more descriptive help message here */} + Failed to load user + + + + )} + + + {usersIsFetching && } + {userError && Error loading user} + {!usersIsFetching && ( + { + return ( + + + +
    + +
    + {values.forwardOption === 'internalAddress' && ( + ({ + value: user.userPrincipalName, + name: `${user.displayName} - ${user.userPrincipalName} `, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="ForwardInternal" + /> + )} + {usersError && Failed to load list of users} +
    +
    + + +
    + +
    + {values.forwardOption === 'ExternalAddress' && ( + + )} +
    +
    + + +
    + +
    +
    +
    + + + + + Edit Forwarding + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + + {postResults.data.Results.map((result, idx) => ( +
  • {result}
  • + ))} +
    + )} +
    + ) + }} + /> + )} +
    +
    + + )} + + ) +} +MailboxForwarding.propTypes = { + refreshFunction: PropTypes.func, +} + +const ForwardingSettings = ({ refresh }) => { + const query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + const [content, setContent] = useState([]) + const [currentRefresh, setCurrentRefresh] = useState('') + const { + data: details, + isFetching, + isSuccess, + error, + } = useGenericPostRequestQuery({ + path: `/api/ListExoRequest`, + values: { + TenantFilter: tenantDomain, + Cmdlet: 'Get-Mailbox', + cmdParams: { Identity: userId }, + Select: 'ForwardingAddress,ForwardingSmtpAddress,DeliverToMailboxAndForward', + refresh: currentRefresh, + }, + }) + + const { + data: users = [], + isFetching: usersIsFetching, + isSuccess: usersSuccess, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + Endpoint: 'users', + TenantFilter: tenantDomain, + $filter: "userType eq 'Member' and proxyAddresses/$count ne 0", + $select: 'id,displayName,userPrincipalName', + $count: true, + }, + }) + + useEffect(() => { + if (refresh !== currentRefresh) { + setCurrentRefresh(refresh) + } + + if (usersSuccess && isSuccess) { + if (details?.Results?.ForwardingAddress !== null) { + var user = null + if ( + details?.Results?.ForwardingAddress.match( + /^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$/, + ) + ) { + const userId = details?.Results?.ForwardingAddress + user = users?.Results?.find((u) => u.id === userId) + } + if (user) { + setContent([ + { + heading: 'Forward and Deliver', + body: formatter(details?.Results?.DeliverToMailboxAndForward, false, false, true), + }, + { + heading: 'Forwarding Address', + body: ( + <> + + Internal + + {user.displayName} + + ), + }, + ]) + } else { + setContent([ + { + heading: 'Forward and Deliver', + body: formatter(details?.Results?.DeliverToMailboxAndForward, false, false, true), + }, + { + heading: 'Forwarding Address', + body: ( + <> + + Internal + + {details?.Results?.ForwardingAddress} + + ), + }, + ]) + } + } else if (details?.Results?.ForwardingSmtpAddress !== null) { + var smtpAddress = details?.Results?.ForwardingSmtpAddress.replace('smtp:', '') + setContent([ + { + heading: 'Forward and Deliver', + body: formatter(details?.Results?.DeliverToMailboxAndForward, false, false, true), + }, + { + heading: 'Forwarding Address', + body: ( + <> + + External + + {smtpAddress} + + ), + }, + ]) + } else { + setContent([ + { + heading: 'Forward and Deliver', + body: formatter(details?.Results?.DeliverToMailboxAndForward, false, false, true), + }, + { + heading: 'Forwarding Address', + body: 'N/A', + }, + ]) + } + } + }, [refresh, currentRefresh, users, details, usersSuccess, isSuccess]) + + return ( + + + {isFetching || usersIsFetching ? ( + <> +
    +
    Forward and Deliver
    +

    + +

    +
    +
    +
    Forwarding Address
    +

    + +

    +
    + + ) : ( + <> + {content.map((item, index) => ( +
    +
    {item.heading}
    +

    {item.body}

    +
    + ))} + + )} +
    + + setCurrentRefresh((Math.random() + 1).toString(36).substring(7))} + color="primary" + variant="ghost" + className="float-end" + > + + + +
    + ) +} +ForwardingSettings.propTypes = { + refresh: PropTypes.string, +} + +const OutOfOffice = ({ refreshFunction }) => { + const dispatch = useDispatch() + let query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + const [queryError, setQueryError] = useState(false) + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + const { + data: user = {}, + isFetching: userIsFetching, + error: userError, + } = useListMailboxPermissionsQuery({ tenantDomain, userId }) + + useEffect(() => { + if (postResults.isSuccess) { + // @TODO do something here? + } + if (!userId || !tenantDomain) { + ModalService.open({ + body: 'Error invalid request, could not load requested user.', + title: 'Invalid Request', + }) + setQueryError(true) + } else { + setQueryError(false) + } + }, [userId, tenantDomain, dispatch, postResults]) + const onSubmit = (values) => { + const shippedValues = { + user: userId, + tenantFilter: tenantDomain, + AutoReplyState: values.AutoReplyState ? 'Scheduled' : 'Disabled', + StartTime: startDate.toUTCString(), + EndTime: endDate.toUTCString(), + InternalMessage: values.InternalMessage ? values.InternalMessage : '', + ExternalMessage: values.ExternalMessage ? values.ExternalMessage : '', + } + //window.alert(JSON.stringify(shippedValues)) + genericPostRequest({ path: '/api/ExecSetOoO', values: shippedValues }).then(() => { + refreshFunction() + }) + } + const initialState = { + ...user, + } + + const formDisabled = queryError === true + + return ( + <> + {!queryError && ( + <> + {queryError && ( + + + + {/* @todo add more descriptive help message here */} + Failed to load user + + + + )} + + + {userIsFetching && } + {userError && Error loading user} + {!userIsFetching && ( + { + return ( + + + + + + + + + + setStartDate(date)} + showTimeSelect + /> + + + + + + setEndDate(date)} + showTimeSelect + /> + + + + + + + + + + + + + + + + Edit Out of Office + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + {postResults.data?.Results} + )} + + ) + }} + /> + )} + + + + )} + + ) +} +OutOfOffice.propTypes = { + refreshFunction: PropTypes.func, +} + +const OutOfOfficeSettings = ({ refresh }) => { + const query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + const tenantFilter = tenantDomain + const [currentRefresh, setCurrentRefresh] = useState('') + + useEffect(() => { + if (refresh !== currentRefresh) { + setCurrentRefresh(refresh) + } + }, [refresh, currentRefresh, setCurrentRefresh]) + + const { + data: details, + isFetching, + error, + } = useGenericGetRequestQuery({ + path: '/api/ListOoO', + params: { userId, tenantFilter, currentRefresh }, + }) + const combinedRegex = /(<([^>]+)>)|| /gi + const content = [ + { + heading: 'Auto Reply State', + body: formatter(details?.AutoReplyState, false, false, true), + }, + { + heading: 'Start Date/Time', + body: details?.StartTime ? details?.StartTime : 'N/A', + }, + { + heading: 'End Date/Time', + body: details?.EndTime ? details?.EndTime : 'N/A', + }, + { + heading: 'Internal Message', + body: details?.InternalMessage ? details?.InternalMessage.replace(combinedRegex, '') : 'N/A', + }, + { + heading: 'External Message', + body: details?.ExternalMessage ? details?.ExternalMessage.replace(combinedRegex, '') : 'N/A', + }, + ] + return ( + + + {isFetching && ( + <> + {content.map((item, index) => ( +
    +
    {item.heading}
    +

    + +

    +
    + ))} + + )} + {!isFetching && ( + <> + {content.map((item, index) => ( +
    +
    {item.heading}
    +

    {item.body}

    +
    + ))} + + )} + + {error && Could not connect to API: {error.message}} +
    + + setCurrentRefresh((Math.random() + 1).toString(36).substring(7))} + color="primary" + variant="ghost" + className="float-end" + > + + + +
    + ) +} +OutOfOfficeSettings.propTypes = { + refresh: PropTypes.string, +} diff --git a/src/views/email-exchange/administration/MailboxRuleList.jsx b/src/views/email-exchange/administration/MailboxRuleList.jsx new file mode 100644 index 000000000000..b941e501e01b --- /dev/null +++ b/src/views/email-exchange/administration/MailboxRuleList.jsx @@ -0,0 +1,127 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { CippPageList } from 'src/components/layout' +import { CellTip } from 'src/components/tables' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CButton } from '@coreui/react' +import { faTrash } from '@fortawesome/free-solid-svg-icons' +import { ModalService } from 'src/components/utilities' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' + +const DeleteMailboxRuleButton = (ruleId, userPrincipalName, ruleName) => { + const tenant = useSelector((state) => state.app.currentTenant) + const [genericGetRequest, getResults] = useLazyGenericGetRequestQuery() + const handleModal = (modalMessage, modalUrl) => { + ModalService.confirm({ + body: ( +
    +
    {modalMessage}
    +
    + ), + title: 'Confirm', + onConfirm: () => genericGetRequest({ path: modalUrl }), + }) + } + return ( + { + ModalService.confirm( + handleModal( + 'Are you sure you want to remove this mailbox rule?', + `/api/ExecRemoveMailboxRule?TenantFilter=${tenant?.defaultDomainName}&ruleId=${ruleId}&ruleName=${ruleName}&userPrincipalName=${userPrincipalName}`, + ), + ) + }} + > + + + ) +} + +const MailboxRuleList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + const columns = [ + { + selector: (row) => row?.Tenant, + name: 'Tenant', + sortable: true, + exportSelector: 'Tenant', + maxWidth: '150px', + cell: cellGenericFormatter(), + }, + { + selector: (row) => row?.UserPrincipalName, + name: 'User Principal Name', + sortable: true, + exportSelector: 'UserPrincipalName', + maxWidth: '200px', + cell: cellGenericFormatter(), + }, + { + selector: (row) => row?.Enabled, + name: 'Enabled', + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'Enabled', + maxWidth: '50px', + }, + { + selector: (row) => row?.Name, + name: 'Display Name', + sortable: true, + cell: cellGenericFormatter(), + maxWidth: '200px', + exportSelector: 'Name', + }, + { + selector: (row) => row?.Description, + name: 'Description', + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'Description', + }, + { + selector: (row) => row?.MailboxOwnerId, + name: 'Mailbox', + sortable: true, + exportSelector: 'MailboxOwnerId', + maxWidth: '150px', + cell: cellGenericFormatter(), + }, + { + selector: (row) => row?.ForwardTo, + name: 'Forwards To', + sortable: true, + exportSelector: 'ForwardTo', + cell: cellGenericFormatter(), + }, + { + name: 'Action', + maxWidth: '100px', + cell: (row) => + DeleteMailboxRuleButton(row['Identity'], row['UserPrincipalName'], row['Name']), + }, + ] + + return ( + // TODO: Add support for displaying the result of the delete operation. Currently, the delete operation is performed but the result is not displayed anywhere but the networking tab of the dev tools in the browser. + // All API code is in place and should return the needed HTTP status information. -Bobby + + ) +} + +export default MailboxRuleList diff --git a/src/views/email-exchange/administration/QuarantineList.jsx b/src/views/email-exchange/administration/QuarantineList.jsx new file mode 100644 index 000000000000..87582178f38d --- /dev/null +++ b/src/views/email-exchange/administration/QuarantineList.jsx @@ -0,0 +1,226 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CippPageList } from 'src/components/layout' +import { CButton, CSpinner, CTooltip } from '@coreui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faEllipsisV, faMinusCircle, faPaperPlane } from '@fortawesome/free-solid-svg-icons' +import { CippActionsOffcanvas, CippOffcanvas } from 'src/components/utilities' +import { cellDateFormatter, CellTip } from 'src/components/tables' +import { MessageViewer } from 'src/views/email-exchange/tools/MessageViewer' +import { ModalService } from 'src/components/utilities' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' +import PropTypes from 'prop-types' +import Skeleton from 'react-loading-skeleton' + +const QuarantineList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + const Offcanvas = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + const [msgOcVisible, setMsgOcVisible] = useState(false) + const [getQuarantineMessage, quarantineMessage] = useLazyGenericGetRequestQuery() + return ( + <> + + { + setMsgOcVisible(true) + getQuarantineMessage({ + path: `/api/ListMailQuarantineMessage`, + params: { TenantFilter: tenant.defaultDomainName, Identity: row?.Identity }, + }) + }} + > + + + + + setOCVisible(true)}> + + + + , + }, + { + label: 'Deny', + color: 'info', + modal: true, + modalUrl: `/api/ExecQuarantineManagement?TenantFilter=${tenant.defaultDomainName}&ID=${row.Identity}&Type=Deny`, + modalMessage: 'Are you sure you want to deny this message?', + icon: , + }, + { + label: 'Release & Allow Sender', + color: 'info', + modal: true, + modalUrl: `/api/ExecQuarantineManagement?TenantFilter=${tenant.defaultDomainName}&ID=${row.Identity}&Type=Release&AllowSender=true`, + modalMessage: + 'Are you sure you want to release this email, and add the sender to the whitelist?', + icon: , + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + setMsgOcVisible(false)} + visible={msgOcVisible} + placement="end" + > + <> + {quarantineMessage.isLoading && } + {quarantineMessage.isSuccess && ( + + )} + + + + ) + } + + //TODO: Add CellBoolean + const columns = [ + { + selector: (row) => row['SenderAddress'], + name: 'Sender', + sortable: true, + cell: (row) => CellTip(row['SenderAddress']), + exportSelector: 'SenderAddress', + }, + { + selector: (row) => row.RecipientAddress.join(', '), + name: 'Recipient', + sortable: true, + exportSelector: 'RecipientAddress', + cell: (row) => CellTip(row.RecipientAddress.join(', ')), + }, + { + selector: (row) => row['Subject'], + name: 'Subject', + sortable: true, + cell: (row) => CellTip(row['Subject']), + exportSelector: 'Subject', + maxWidth: '300px', + }, + { + selector: (row) => row['Type'], + name: 'Reason', + sortable: true, + exportSelector: 'Type', + maxWidth: '200px', + }, + { + selector: (row) => row['ReceivedTime'], + name: 'Received on', + sortable: true, + exportSelector: 'ReceivedTime', + maxWidth: '150px', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['ReleaseStatus'], + name: 'Status', + sortable: true, + exportSelector: 'ReleaseStatus', + maxWidth: '150px', + }, + { + selector: (row) => row['PolicyName'], + name: 'Blocked by Policy', + sortable: true, + exportSelector: 'PolicyName', + maxWidth: '170px', + cell: (row) => CellTip(row['PolicyName']), + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '100px', + }, + ] + + return ( + + ) +} + +export default QuarantineList diff --git a/src/views/email-exchange/connectors/ConnectorList.jsx b/src/views/email-exchange/connectors/ConnectorList.jsx new file mode 100644 index 000000000000..c5c2b2d0951a --- /dev/null +++ b/src/views/email-exchange/connectors/ConnectorList.jsx @@ -0,0 +1,179 @@ +import { CButton } from '@coreui/react' +import { faBan, faBook, faCheck, faEllipsisV, faTrash } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CippPageList } from 'src/components/layout' +import { CippActionsOffcanvas } from 'src/components/utilities' +import { CellBoolean, cellBooleanFormatter, CellTip } from 'src/components/tables' +import { TitleButton } from 'src/components/buttons' + +const Offcanvas = (row, rowIndex, formatExtraData) => { + const tenant = useSelector((state) => state.app.currentTenant) + const [ocVisible, setOCVisible] = useState(false) + //console.log(row) + return ( + <> + setOCVisible(true)}> + + + , + modalBody: row, + modalType: 'POST', + modalUrl: `/api/AddExConnectorTemplate`, + modalMessage: 'Are you sure you want to create a template based on this connector?', + }, + { + label: 'Enable Connector', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditExConnector?State=Enable&TenantFilter=${tenant.defaultDomainName}&GUID=${row.Guid}&Type=${row.cippconnectortype}`, + modalMessage: 'Are you sure you want to enable this connector?', + }, + { + label: 'Disable Connector', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditExConnector?State=Disable&TenantFilter=${tenant.defaultDomainName}&GUID=${row.Guid}&Type=${row.cippconnectortype}`, + modalMessage: 'Are you sure you want to disable this connector?', + }, + { + label: 'Delete Connector', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemoveExConnector?TenantFilter=${tenant.defaultDomainName}&GUID=${row.Guid}&Type=${row.cippconnectortype}`, + modalMessage: 'Are you sure you want to delete this connector?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const columns = [ + { + name: 'Name', + selector: (row) => row['Name'], + sortable: true, + wrap: true, + cell: (row) => CellTip(row['Name']), + exportSelector: 'Name', + }, + { + name: 'State', + selector: (row) => row['Enabled'], + cell: cellBooleanFormatter(), + sortable: true, + exportSelector: 'Enabled', + }, + { + name: 'Comment', + selector: (row) => row['Comment'], + sortable: true, + exportSelector: 'Comment', + }, + { + name: 'Direction', + selector: (row) => row['cippconnectortype'], + sortable: true, + exportSelector: 'cippconnectortype', + }, + { + name: 'Inbound Connector Hostname', + selector: (row) => row['TlsSenderCertificateName'], + exportSelector: 'TlsSenderCertificateName', + }, + { + name: 'Sender IP Addresses', + selector: (row) => row.SenderIPAddresses?.join(','), + exportSelector: 'SenderIPAddresses', + }, + { + name: 'Only apply via transport rules', + selector: (row) => row.IsTransportRuleScoped, + exportSelector: 'IsTransportRuleScoped', + cell: cellBooleanFormatter(), + }, + { + name: 'Smarthost', + selector: (row) => row.SmartHosts?.join(','), + exportSelector: 'Smarthost', + }, + { + name: 'TLS Settings', + selector: (row) => row.TlsSettings, + exportSelector: 'TlsSettings', + }, + { + name: 'TLS Domain', + selector: (row) => row.TlsDomain, + exportSelector: 'Smarthost', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '80px', + }, +] + +const ConnectorList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + return ( + + + + } + tenantSelector={true} + datatable={{ + filterlist: [ + { filterName: 'Enabled connectors', filter: 'Complex: Enabled eq true' }, + { filterName: 'Disabled connectors', filter: 'Complex: Enabled eq false' }, + { filterName: 'Inbound connectors', filter: 'Complex: cippconnectortype eq inbound' }, + { filterName: 'Outbound connectors', filter: 'Complex: cippconnectortype eq outbound' }, + { + filterName: 'Transport rule connectors', + filter: 'Complex: IsTransportRuleScoped eq true', + }, + { + filterName: 'Non-transport rule connectors', + filter: 'Complex: IsTransportRuleScoped eq false', + }, + ], + reportName: `${tenant?.defaultDomainName}-connectors-list`, + path: '/api/ListExchangeConnectors', + params: { TenantFilter: tenant?.defaultDomainName }, + columns, + }} + /> + ) +} + +export default ConnectorList diff --git a/src/views/email-exchange/tools/MessageViewer.jsx b/src/views/email-exchange/tools/MessageViewer.jsx new file mode 100644 index 000000000000..bc63e647b186 --- /dev/null +++ b/src/views/email-exchange/tools/MessageViewer.jsx @@ -0,0 +1,342 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import PropTypes from 'prop-types' +import { CippPage, CippMasonry, CippMasonryItem, CippContentCard } from 'src/components/layout' +import { parseEml, readEml, GBKUTF8, decode } from 'eml-parse-js' +import { useMediaPredicate } from 'react-media-hook' +import { useSelector } from 'react-redux' +import { CellDate } from 'src/components/tables' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + CButton, + CCard, + CCardBody, + CCol, + CDropdown, + CDropdownMenu, + CDropdownToggle, + CLink, + CRow, +} from '@coreui/react' +import ReactTimeAgo from 'react-time-ago' +import { CippCodeBlock, ModalService } from 'src/components/utilities' +import DOMPurify from 'dompurify' +import ReactHtmlParser from 'react-html-parser' +import CippDropzone from 'src/components/utilities/CippDropzone' + +export const MessageViewer = ({ emailSource }) => { + const [emlContent, setEmlContent] = useState(null) + const [emlError, setEmlError] = useState(false) + const [messageHtml, setMessageHtml] = useState('') + const [emlHeaders, setEmlHeaders] = useState(null) + + const getAttachmentIcon = (contentType) => { + if (contentType.includes('image')) { + return 'image' + } else if (contentType.includes('audio')) { + return 'volume-up' + } else if (contentType.includes('video')) { + return 'video' + } else if (contentType.includes('text')) { + return 'file-lines' + } else if (contentType.includes('pdf')) { + return 'file-pdf' + } else if ( + contentType.includes('zip') || + contentType.includes('compressed') || + contentType.includes('tar') || + contentType.includes('gzip') + ) { + return 'file-zipper' + } else if (contentType.includes('msword')) { + return 'file-word' + } else if (contentType.includes('spreadsheet')) { + return 'file-excel' + } else if (contentType.includes('presentation')) { + return 'file-powerpoint' + } else if (contentType.includes('json') || contentType.includes('xml')) { + return 'file-code' + } else if (contentType.includes('rfc822')) { + return 'envelope' + } else { + return 'file' + } + } + + const downloadAttachment = (attachment, newTab = false) => { + var contentType = attachment?.contentType?.split(';')[0] ?? 'text/plain' + var fileBytes = attachment.data + if (fileBytes instanceof Uint8Array && attachment?.data64) { + fileBytes = new Uint8Array( + atob(attachment.data64) + .split('') + .map((c) => c.charCodeAt(0)), + ) + } + var fileName = attachment.name + const blob = new Blob([fileBytes], { type: contentType ?? 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + if (newTab) { + if (contentType.includes('rfc822')) { + var content = fileBytes + const nestedMessage = + ModalService.open({ + body: nestedMessage, + title: fileName, + size: 'lg', + }) + } else if (contentType.includes('pdf')) { + const embeddedPdf = + ModalService.open({ + body: embeddedPdf, + title: fileName, + size: 'lg', + }) + } else if (contentType.includes('image')) { + const embeddedImage = {fileName} + ModalService.open({ + body: embeddedImage, + title: fileName, + size: 'lg', + }) + } else if (contentType.includes('text')) { + const textContent = fileBytes + ModalService.open({ + data: textContent, + componentType: 'codeblock', + title: fileName, + size: 'lg', + }) + setTimeout(() => { + URL.revokeObjectURL(url) + }, 1000) + } else { + const newWindow = window.open() + newWindow.location.href = url + URL.revokeObjectURL(url) + } + } else { + link.href = url + link.download = fileName + link.click() + URL.revokeObjectURL(url) + } + } + + function isValidDate(d) { + return d instanceof Date && !isNaN(d) + } + + const showEmailModal = (emailSource, title = 'Email Source') => { + ModalService.open({ + data: emailSource, + componentType: 'codeblock', + title: title, + size: 'lg', + }) + } + + const EmailButtons = (emailHeaders, emailSource) => { + const emailSourceBytes = new TextEncoder().encode(emailSource) + const blob = new Blob([emailSourceBytes], { type: 'message/rfc822' }) + const url = URL.createObjectURL(blob) + return ( + + {emailHeaders && ( + showEmailModal(emailHeaders, 'Email Headers')} className="me-2"> + + View Headers + + )} + showEmailModal(emailSource)}> + + View Source + + + ) + } + + useEffect(() => { + readEml(emailSource, (err, ReadEmlJson) => { + if (err) { + setEmlError(true) + setEmlContent(null) + setMessageHtml(null) + setEmlHeaders(null) + } else { + setEmlContent(ReadEmlJson) + setEmlError(false) + if (ReadEmlJson.html) { + var sanitizedHtml = DOMPurify.sanitize(ReadEmlJson.html) + var parsedHtml = ReactHtmlParser(sanitizedHtml) + setMessageHtml(parsedHtml) + } else { + setMessageHtml(null) + } + const header_regex = /(?:^[\w-]+:\s?.*(?:\r?\n[ \t].*)*\r?\n?)+/gm + const headers = emailSource.match(header_regex) + setEmlHeaders(headers ? headers[0] : null) + } + }) + }, [emailSource, setMessageHtml, setEmlError, setEmlContent, setEmlHeaders]) + + var buttons = EmailButtons(emlHeaders, emailSource) + + return ( + <> + {emlError && ( + + Unable to parse the EML file, email source is displayed below. + + + )} + + {emlContent && ( + <> + + <> + + +
    + + {emlContent?.from?.name} <{emlContent?.from?.email}> +
    + {emlContent?.to?.length > 0 && ( +
    + + To:{' '} + {emlContent?.to?.map((to) => to.name + ' <' + to.email + '>').join(', ')} + +
    + )} + {emlContent?.cc?.length > 0 && ( +
    + + CC:{' '} + {emlContent?.cc?.map((cc) => cc.name + ' <' + cc.email + '>').join(', ')} + +
    + )} +
    + +
    + + + {emlContent.date && isValidDate(emlContent.date) + ? emlContent.date.toLocaleDateString() + : 'Invalid Date'} + + {emlContent.date && isValidDate(emlContent.date) && ( + <> + () + + )} + +
    +
    +
    + + + {emlContent.attachments && emlContent.attachments.length > 0 && ( + + + {emlContent.attachments.map((attachment, index) => ( + + + + {attachment.name ?? 'No name'} + + + downloadAttachment(attachment)} + > + + Download + + {(attachment?.contentType === undefined || + attachment?.contentType?.includes('text') || + attachment?.contentType?.includes('pdf') || + attachment?.contentType?.includes('image') || + attachment?.contentType?.includes('rfc822')) && ( + downloadAttachment(attachment, true)} + > + + View + + )} + + + ))} + + + )} + + {(emlContent?.text || emlContent?.html) && ( + + + {messageHtml ? ( +
    {messageHtml}
    + ) : ( +
    + +
    + )} +
    +
    + )} +
    + + )} + + ) +} + +MessageViewer.propTypes = { + emailSource: PropTypes.string, +} + +const MessageViewerPage = () => { + const [emlFile, setEmlFile] = useState(null) + const onDrop = useCallback((acceptedFiles) => { + acceptedFiles.forEach((file) => { + const reader = new FileReader() + reader.onabort = () => console.log('file reading was aborted') + reader.onerror = () => console.log('file reading has failed') + reader.onload = () => { + setEmlFile(reader.result) + } + reader.readAsText(file) + }) + }, []) + + return ( + + + {emlFile && } + + ) +} + +export default MessageViewerPage diff --git a/src/views/email-exchange/transport/TransportRules.jsx b/src/views/email-exchange/transport/TransportRules.jsx new file mode 100644 index 000000000000..549f245a2ea5 --- /dev/null +++ b/src/views/email-exchange/transport/TransportRules.jsx @@ -0,0 +1,147 @@ +import { CButton } from '@coreui/react' +import { faBan, faBook, faCheck, faEllipsisV, faTrash } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CippPageList } from 'src/components/layout' +import { CippActionsOffcanvas } from 'src/components/utilities' +import { CellTip } from 'src/components/tables' +import { TitleButton } from 'src/components/buttons' + +const Offcanvas = (row, rowIndex, formatExtraData) => { + const tenant = useSelector((state) => state.app.currentTenant) + const [ocVisible, setOCVisible] = useState(false) + //console.log(row) + return ( + <> + setOCVisible(true)}> + + + , + modalBody: row, + modalType: 'POST', + modalUrl: `/api/AddTransportTemplate`, + modalMessage: 'Are you sure you want to create a template based on this rule?', + }, + { + label: 'Enable Rule', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditTransportRule?State=Enable&TenantFilter=${tenant.defaultDomainName}&GUID=${row.Guid}`, + modalMessage: 'Are you sure you want to enable this rule?', + }, + { + label: 'Disable Rule', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditTransportRule?State=Disable&TenantFilter=${tenant.defaultDomainName}&GUID=${row.Guid}`, + modalMessage: 'Are you sure you want to disable this rule?', + }, + { + label: 'Delete Rule', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemoveTransportRule?TenantFilter=${tenant.defaultDomainName}&GUID=${row.Guid}`, + modalMessage: 'Are you sure you want to delete this rule?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const columns = [ + { + name: 'Name', + selector: (row) => row['Name'], + sortable: true, + wrap: true, + cell: (row) => CellTip(row['Name']), + exportSelector: 'Name', + }, + { + name: 'State', + selector: (row) => row['State'], + sortable: true, + exportSelector: 'State', + }, + { + name: 'Mode', + selector: (row) => row['Mode'], + sortable: true, + exportSelector: 'Mode', + }, + { + name: 'Error Action', + selector: (row) => row['RuleErrorAction'], + sortable: true, + exportSelector: 'RuleErrorAction', + }, + { + name: 'description', + selector: (row) => row['Description'], + omit: true, + exportSelector: 'Description', + }, + { + name: 'GUID', + selector: (row) => row['Guid'], + omit: true, + exportSelector: 'Guid', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '80px', + }, +] + +const TransportRulesList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + return ( + + + + } + datatable={{ + filterlist: [ + { filterName: 'Enabled rules', filter: 'Complex: State eq Enabled' }, + { filterName: 'Disabled rules', filter: 'Complex: State eq Disabled' }, + ], + reportName: `${tenant?.defaultDomainName}-transport-rules-list`, + path: '/api/ListTransportRules', + params: { TenantFilter: tenant?.defaultDomainName }, + columns, + }} + /> + ) +} + +export default TransportRulesList diff --git a/src/views/endpoint/applications/ApplicationsAddRMM.jsx b/src/views/endpoint/applications/ApplicationsAddRMM.jsx new file mode 100644 index 000000000000..e90e31ff78e3 --- /dev/null +++ b/src/views/endpoint/applications/ApplicationsAddRMM.jsx @@ -0,0 +1,366 @@ +import React from 'react' +import { CCol, CRow, CForm, CCallout, CSpinner } from '@coreui/react' +import { Field, FormSpy } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' +import { CippWizard } from 'src/components/layout' +import { WizardTableField } from 'src/components/tables' +import PropTypes from 'prop-types' +import { Condition, RFFCFormInput, RFFCFormRadio, RFFSelectSearch } from 'src/components/forms' +import { useLazyGenericPostRequestQuery } from 'src/store/api/app' + +const Error = ({ name }) => ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +const requiredArray = (value) => (value && value.length !== 0 ? undefined : 'Required') + +const AddRMM = () => { + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const handleSubmit = async (values) => { + values.selectedTenants.map( + (tenant) => (values[`Select_${tenant.defaultDomainName}`] = tenant.defaultDomainName), + ) + if (values.AssignTo === 'customGroup') { + values.AssignTo = values.customGroup + } + genericPostRequest({ path: '/api/AddMSPApp', values: values }) + } + + const formValues = { + arch: true, + RemoveVersions: true, + AcceptLicense: true, + AssignTo: 'On', + } + + return ( + + +
    +

    Step 1

    +
    Choose a tenant
    +
    +
    + + {(props) => ( + row['displayName'], + sortable: true, + exportselector: 'displayName', + }, + { + name: 'Default Domain Name', + selector: (row) => row['defaultDomainName'], + sortable: true, + exportselector: 'mail', + }, + ]} + fieldProps={props} + /> + )} + + +
    +
    + +
    +

    Step 2

    +
    Supply the app information
    +
    +
    + + + + + + + + + + + {/* eslint-disable react/prop-types */} + {(props) => { + return ( + <> + + + + + + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + + + + + + + + + + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + + + + + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + + + + + + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + + + + + + + + + + + + + + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + + {props.values.selectedTenants.map((item, index) => ( + + + + ))} + + + ) + }} + + + + + + + + + + +
    +
    + +
    +

    Step 3

    +
    Confirm and apply
    +
    +
    + {!postResults.isSuccess && ( + + {(props) => { + return ( + <> + + + + + + ) + }} + + )} + {postResults.isFetching && ( + + Loading + + )} + {postResults.isSuccess && {postResults.data.Results}} +
    +
    +
    + ) +} + +export default AddRMM diff --git a/src/views/endpoint/applications/ListApplicationQueue.jsx b/src/views/endpoint/applications/ListApplicationQueue.jsx new file mode 100644 index 000000000000..eb42ed86ec47 --- /dev/null +++ b/src/views/endpoint/applications/ListApplicationQueue.jsx @@ -0,0 +1,139 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { CSpinner, CButton, CCallout } from '@coreui/react' +import { faCheck, faExclamationTriangle, faTrash } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CippPageList } from 'src/components/layout' +import { ModalService } from 'src/components/utilities' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' +import { CellTip } from 'src/components/tables' + +const RefreshAction = () => { + const [execStandards, execStandardsResults] = useLazyGenericGetRequestQuery() + + const showModal = () => + ModalService.confirm({ + body: ( +
    + Deploy all queued applications to tenants? +
    + Please note: This job runs automatically every 12 hours. +
    + ), + onConfirm: () => execStandards({ path: 'api/ExecAppUpload' }), + }) + + return ( + <> + {execStandardsResults.data?.Results === + 'Already running. Please wait for the current instance to finish' && ( +
    {execStandardsResults.data?.Results}
    + )} + + {execStandardsResults.isLoading && } + {execStandardsResults.error && ( + + )} + {execStandardsResults.isSuccess && } + Deploy now + + + ) +} +const ListApplicationQueue = () => { + const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery() + const Actions = (row, index, column) => { + const handleDeleteStandard = (apiurl, message) => { + ModalService.confirm({ + title: 'Confirm', + body:
    {message}
    , + onConfirm: () => ExecuteGetRequest({ path: apiurl }), + confirmLabel: 'Continue', + cancelLabel: 'Cancel', + }) + } + return ( + + handleDeleteStandard( + `api/RemoveQueuedApp?ID=${row.id}`, + 'Do you want to delete the queued application?', + ) + } + > + + + ) + } + const columns = [ + { + name: 'Tenant', + selector: (row) => row['tenantName'], + sortable: true, + cell: (row) => CellTip(row['tenantName']), + exportSelector: 'tenantName', + minWidth: '200px', + }, + { + name: 'Application Name', + selector: (row) => row['applicationName'], + sortable: true, + cell: (row) => CellTip(row['applicationName']), + exportSelector: 'applicationName', + minWidth: '200px', + }, + { + name: 'Install command', + selector: (row) => row['cmdLine'], + sortable: true, + cell: (row) => CellTip(row['cmdLine']), + exportSelector: 'cmdLine', + }, + { + name: 'Assign To', + selector: (row) => row['assignTo'], + sortable: true, + cell: (row) => CellTip(row['assignTo']), + exportSelector: 'assignTo', + }, + { + name: 'Actions', + cell: Actions, + }, + ] + const tenant = useSelector((state) => state.app.currentTenant) + + return ( +
    + {getResults.isFetching && ( + + Loading + + )} + {getResults.isSuccess && {getResults.data?.Results}} + {getResults.isError && ( + Could not connect to API: {getResults.error.message} + )} + ], + }, + keyField: 'id', + columns, + reportName: `ApplicationQueue-List`, + path: '/api/ListApplicationQueue', + params: { TenantFilter: tenant?.defaultDomainName }, + }} + /> +
    + ) +} + +export default ListApplicationQueue diff --git a/src/views/endpoint/intune/MEMListCompliance.jsx b/src/views/endpoint/intune/MEMListCompliance.jsx new file mode 100644 index 000000000000..87c2fdfabdf3 --- /dev/null +++ b/src/views/endpoint/intune/MEMListCompliance.jsx @@ -0,0 +1,164 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CButton } from '@coreui/react' +import { + faBook, + faEdit, + faEllipsisV, + faGlobeEurope, + faPager, + faTrashAlt, + faUser, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CippPageList } from 'src/components/layout' +import { Link } from 'react-router-dom' +import { CippActionsOffcanvas, CippCodeBlock } from 'src/components/utilities' +import { TitleButton } from 'src/components/buttons' +import { cellBooleanFormatter, cellDateFormatter } from 'src/components/tables' + +const Actions = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + const tenant = useSelector((state) => state.app.currentTenant) + return ( + <> + setOCVisible(true)}> + + + , + modalUrl: `/api/AddIntuneTemplate?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=deviceCompliancePolicies`, + modalMessage: 'Are you sure you want to create a template based on this policy?', + }, + { + icon: , + label: ' Assign to All Users', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=allLicensedUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=deviceCompliancePolicies`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all users?`, + }, + { + icon: , + label: ' Assign to All Devices', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevices&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=deviceCompliancePolicies`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all devices?`, + }, + { + icon: , + label: ' Assign Globally (All Users / All Devices)', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevicesAndUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=deviceCompliancePolicies`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all users and devices?`, + }, + { + label: 'Delete Policy', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=deviceCompliancePolicies`, + modalMessage: 'Are you sure you want to delete this policy?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const columns = [ + { + selector: (row) => row['displayName'], + name: 'Name', + sortable: true, + exportSelector: 'displayName', + }, + { + selector: (row) => row['description'], + name: 'Description', + sortable: true, + exportSelector: 'description', + }, + { + selector: (row) => row['lastModifiedDateTime'], + name: 'Last Modified', + exportSelector: 'lastModifiedDateTime', + cell: cellDateFormatter({ format: 'relative' }), + }, + { + name: 'Actions', + cell: Actions, + maxWidth: '80px', + }, +] + +const ComplianceList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + // eslint-disable-next-line react/prop-types + const ExpandedComponent = ({ data }) => ( + // eslint-disable-next-line react/prop-types + + ) + + return ( + + + + } + tenantSelector={true} + datatable={{ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenant?.defaultDomainName, + Endpoint: 'deviceManagement/deviceCompliancePolicies', + $orderby: 'displayName', + $count: true, + $expand: 'assignments', + }, + columns, + reportName: `${tenant?.defaultDomainName}-MEMPolicies-List`, + tableProps: { + expandableRows: true, + expandableRowsComponent: ExpandedComponent, + expandOnRowClicked: true, + selectableRows: true, + actionsList: [ + { + label: 'Delete Policy', + modal: true, + modalUrl: `api/RemovePolicy?TenantFilter=${tenant?.defaultDomainName}&ID=!id&URLName=deviceCompliancePolicies`, + modalMessage: 'Are you sure you want to delete these policies?', + }, + ], + }, + }} + /> + ) +} + +export default ComplianceList diff --git a/src/views/endpoint/intune/MEMListPolicies.jsx b/src/views/endpoint/intune/MEMListPolicies.jsx new file mode 100644 index 000000000000..725c4cc0cd42 --- /dev/null +++ b/src/views/endpoint/intune/MEMListPolicies.jsx @@ -0,0 +1,190 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { CButton } from '@coreui/react' +import { + faBook, + faEdit, + faEllipsisV, + faGlobeEurope, + faPager, + faTrashAlt, + faUser, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CippPageList } from 'src/components/layout' +import { Link } from 'react-router-dom' +import { CippActionsOffcanvas, CippCodeBlock } from 'src/components/utilities' +import { TitleButton } from 'src/components/buttons' +import { cellBooleanFormatter, cellDateFormatter } from 'src/components/tables' + +const Actions = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + + const tenant = useSelector((state) => state.app.currentTenant) + return ( + <> + setOCVisible(true)}> + + + , + modalUrl: `/api/AddIntuneTemplate?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=${row.URLName}`, + modalMessage: 'Are you sure you want to create a template based on this policy?', + }, + { + icon: , + label: ' Assign to All Users', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=allLicensedUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=${row.URLName}`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all users?`, + }, + { + icon: , + label: ' Assign to All Devices', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevices&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=${row.URLName}`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all devices?`, + }, + { + icon: , + label: ' Assign Globally (All Users / All Devices)', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignPolicy?AssignTo=AllDevicesAndUsers&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&type=${row.URLName}`, + modalMessage: `Are you sure you want to assign ${row.displayName} to all users and devices?`, + }, + { + label: 'Delete Policy', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=${row.URLName}`, + modalMessage: 'Are you sure you want to delete this policy?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const columns = [ + { + selector: (row) => row['displayName'], + name: 'Name', + sortable: true, + exportSelector: 'displayName', + maxWidth: 'auto', + }, + { + selector: (row) => row['PolicyTypeName'], + name: 'Profile Type', + sortable: true, + exportSelector: 'PolicyTypeName', + maxWidth: '300px', + }, + { + selector: (row) => row['PolicyAssignment'], + name: 'Assigned', + sortable: true, + exportSelector: 'PolicyAssignment', + maxWidth: '300px', + }, + { + selector: (row) => row['PolicyExclude'], + name: 'Excluded', + sortable: true, + exportSelector: 'PolicyExclude', + maxWidth: '300px', + }, + { + selector: (row) => row['description'], + name: 'Description', + sortable: true, + exportSelector: 'description', + maxWidth: 'auto', + }, + { + selector: (row) => row['lastModifiedDateTime'], + name: 'Last Modified', + sortable: true, + exportSelector: 'lastModifiedDateTime', + cell: cellDateFormatter({ format: 'relative' }), + maxWidth: '150px', + }, + { + selector: (row) => row['id'], + name: 'id', + omit: true, + exportSelector: 'id', + }, + { + name: 'Actions', + cell: Actions, + maxWidth: '80px', + }, +] + +const IntuneList = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + // eslint-disable-next-line react/prop-types + const ExpandedComponent = ({ data }) => ( + // eslint-disable-next-line react/prop-types + + ) + + return ( + + + + } + tenantSelector={true} + datatable={{ + path: '/api/ListIntunePolicy?type=ESP', + params: { TenantFilter: tenant?.defaultDomainName }, + columns, + reportName: `${tenant?.defaultDomainName}-MEMPolicies-List`, + tableProps: { + expandableRows: true, + expandableRowsComponent: ExpandedComponent, + expandOnRowClicked: true, + selectableRows: true, + actionsList: [ + { + label: 'Delete Policy', + modal: true, + modalUrl: `api/RemovePolicy?TenantFilter=${tenant?.defaultDomainName}&ID=!id&URLName=!URLName`, + modalMessage: 'Are you sure you want to delete these policies?', + }, + ], + }, + }} + /> + ) +} + +export default IntuneList diff --git a/src/views/identity/administration/DeployJITAdmin.jsx b/src/views/identity/administration/DeployJITAdmin.jsx new file mode 100644 index 000000000000..b8d5945f001b --- /dev/null +++ b/src/views/identity/administration/DeployJITAdmin.jsx @@ -0,0 +1,365 @@ +import React, { useState } from 'react' +import { CButton, CCallout, CCol, CForm, CRow, CSpinner, CTooltip } from '@coreui/react' +import { useSelector } from 'react-redux' +import { Field, Form, FormSpy } from 'react-final-form' +import { + Condition, + RFFCFormInput, + RFFCFormRadioList, + RFFCFormSwitch, + RFFSelectSearch, + RFFCFormSelect, +} from 'src/components/forms' +import { + useGenericGetRequestQuery, + useLazyGenericGetRequestQuery, + useLazyGenericPostRequestQuery, +} from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleNotch, faEdit, faEye } from '@fortawesome/free-solid-svg-icons' +import { CippContentCard, CippPage, CippPageList } from 'src/components/layout' +import { CellTip, cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import 'react-datepicker/dist/react-datepicker.css' +import { TenantSelector } from 'src/components/utilities' +import arrayMutators from 'final-form-arrays' +import DatePicker from 'react-datepicker' +import { useListUsersQuery } from 'src/store/api/users' +import GDAPRoles from 'src/data/GDAPRoles' +import { CippDatatable, cellDateFormatter } from 'src/components/tables' +import { useListDomainsQuery } from 'src/store/api/domains' + +const DeployJITAdmin = () => { + const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery() + const currentDate = new Date() + const [startDate, setStartDate] = useState(currentDate) + const [endDate, setEndDate] = useState(currentDate) + + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const [refreshState, setRefreshState] = useState(false) + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const { + data: domains = [], + isFetching: domainsIsFetching, + error: domainsError, + } = useListDomainsQuery({ tenantDomain }) + + const onSubmit = (values) => { + const startTime = Math.floor(startDate.getTime() / 1000) + const endTime = Math.floor(endDate.getTime() / 1000) + + const shippedValues = { + TenantFilter: tenantDomain, + UserId: values.UserId?.value.id, + UserPrincipalName: values.username + ? `${values.username}@${values.domain}` + : values.UserId?.value.userPrincipalName, + FirstName: values.FirstName, + LastName: values.LastName, + useraction: values.useraction, + AdminRoles: values.AdminRoles?.map((role) => role.value), + StartDate: startTime, + UseTAP: values.useTap, + EndDate: endTime, + ExpireAction: values.expireAction.value, + PostExecution: { + Webhook: values.webhook, + Email: values.email, + PSA: values.psa, + }, + } + genericPostRequest({ path: '/api/ExecJITAdmin', values: shippedValues }).then((res) => { + setRefreshState(res.requestId) + }) + } + + const { + data: users = [], + isFetching: usersIsFetching, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenantDomain, + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName,accountEnabled', + $count: true, + $top: 999, + $orderby: 'displayName', + }, + }) + + return ( + + <> + + + + { + return ( + +

    + JIT Admin creates an account that is usable for a specific period of time. + Enter a username, select admin roles, date range and expiration action. +

    + + + + + {(props) => } + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + {domainsIsFetching && } + {!domainsIsFetching && ( + ({ + value: domain.id, + label: domain.id, + }))} + /> + )} + {domainsError && Failed to load list of domains} + + + + + + + ({ + value: { userPrincipalName: user.userPrincipalName, id: user.id }, + name: `${user.displayName} <${user.userPrincipalName}>`, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="UserId" + isLoading={usersIsFetching} + /> + + {({ values }) => { + return users?.Results?.map((user, key) => { + if ( + user.id === values?.UserId?.value && + user.accountEnabled === false + ) { + return ( + + This user is currently disabled, they will automatically be + enabled when JIT is executed. + + ) + } + }) + }} + + + + +
    + + + ({ + value: role.ObjectId, + name: role.Name, + }))} + multi={true} + placeholder="Select Roles" + name="AdminRoles" + /> + + + + + + setStartDate(date)} + /> + + + + setEndDate(date)} + /> + + + + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + + + + + + Add JIT Admin + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + + {postResults.data?.Results.map((result, idx) => ( +
  • {result}
  • + ))} +
    + )} + {getResults.isFetching && ( + + Loading + + )} + {getResults.isSuccess && ( + {getResults.data?.Results} + )} + {getResults.isError && ( + + Could not connect to API: {getResults.error.message} + + )} +
    + ) + }} + /> +
    +
    + + + row['userPrincipalName'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'userPrincipalName', + }, + { + name: 'Account Enabled', + selector: (row) => row['accountEnabled'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'accountEnabled', + }, + { + name: 'JIT Enabled', + selector: (row) => row['jitAdminEnabled'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'jitAdminEnabled', + }, + { + name: 'JIT Expires', + selector: (row) => row['jitAdminExpiration'], + sortable: true, + cell: cellDateFormatter({ format: 'short' }), + exportSelector: 'jitAdminExpiration', + }, + { + name: 'Admin Roles', + selector: (row) => row['memberOf'], + sortable: false, + cell: cellGenericFormatter(), + exportSelector: 'memberOf', + }, + ]} + /> + + +
    + +
    + ) +} + +export default DeployJITAdmin diff --git a/src/views/identity/administration/Devices.jsx b/src/views/identity/administration/Devices.jsx new file mode 100644 index 000000000000..f7808c39034a --- /dev/null +++ b/src/views/identity/administration/Devices.jsx @@ -0,0 +1,201 @@ +import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import { CellTip, cellBooleanFormatter } from 'src/components/tables' +import { CippPageList } from 'src/components/layout' +import { Link } from 'react-router-dom' +import { CButton } from '@coreui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faEdit, faEllipsisV } from '@fortawesome/free-solid-svg-icons' +import { CippActionsOffcanvas } from 'src/components/utilities' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' + +const DevicesList = () => { + const [tenantColumnSet, setTenantColumn] = useState(true) + const tenant = useSelector((state) => state.app.currentTenant) + const Offcanvas = (row, rowIndex, formatExtraData) => { + const tenant = useSelector((state) => state.app.currentTenant) + const [ocVisible, setOCVisible] = useState(false) + const editLink = `/identity/administration/groups/edit?groupId=${row.id}&tenantDomain=${tenant.defaultDomainName}` + return ( + <> + setOCVisible(true)}> + + + setOCVisible(false)} + /> + + ) + } + const columns = [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + cell: (row) => CellTip(row['Tenant']), + exportSelector: 'Tenant', + omit: tenantColumnSet, + }, + { + name: 'Retrieval Status', + selector: (row) => row['CippStatus'], + sortable: true, + cell: (row) => CellTip(row['CippStatus']), + exportSelector: 'CippStatus', + omit: tenantColumnSet, + }, + { + selector: (row) => row['displayName'], + name: 'Display Name', + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + }, + { + selector: (row) => row['accountEnabled'], + name: 'Enabled', + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'accountEnabled', + }, + { + selector: (row) => row['deviceOwnership'], + name: 'Device Ownership', + sortable: true, + cell: (row) => CellTip(row['deviceOwnership']), + exportSelector: 'recipientType', + }, + { + selector: (row) => row['enrollmentType'], + name: 'Enrollment Type', + sortable: true, + exportSelector: 'enrollmentType', + }, + { + selector: (row) => row['manufacturer'], + name: 'Manufacturer', + sortable: true, + exportSelector: 'manufacturer', + }, + { + selector: (row) => row['model'], + name: 'Model', + sortable: true, + exportSelector: 'model', + }, + { + selector: (row) => row['operatingSystem'], + name: 'OS', + sortable: true, + exportSelector: 'operatingSystem', + }, + { + selector: (row) => row['operatingSystemVersion'], + name: 'Version', + sortable: true, + exportSelector: 'operatingSystemVersion', + }, + { + selector: (row) => row['profileType'], + name: 'Profile Type', + sortable: true, + exportSelector: 'profileType', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '20px', + }, + ] + useEffect(() => { + if (tenant.defaultDomainName === 'AllTenants') { + setTenantColumn(false) + } + if (tenant.defaultDomainName !== 'AllTenants') { + setTenantColumn(true) + } + }, [tenant.defaultDomainName, tenantColumnSet]) + return ( + + ) +} + +export default DevicesList diff --git a/src/views/identity/administration/UserMailboxRuleList.jsx b/src/views/identity/administration/UserMailboxRuleList.jsx new file mode 100644 index 000000000000..f0a1e05a4d31 --- /dev/null +++ b/src/views/identity/administration/UserMailboxRuleList.jsx @@ -0,0 +1,140 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CellBoolean, cellBooleanFormatter, CellTip } from 'src/components/tables' +import { DatatableContentCard } from 'src/components/contentcards' +import { faEnvelope, faTrash } from '@fortawesome/free-solid-svg-icons' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { CButton } from '@coreui/react' +import { ModalService } from 'src/components/utilities' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' + +const rowStyle = (row, rowIndex) => { + const style = {} + + return style +} + +const DeleteMailboxRuleButton = (tenantDomain, ruleId, userPrincipalName, ruleName) => { + const [genericGetRequest, getResults] = useLazyGenericGetRequestQuery() + const handleModal = (modalMessage, modalUrl) => { + ModalService.confirm({ + body: ( +
    +
    {modalMessage}
    +
    + ), + title: 'Confirm', + onConfirm: () => genericGetRequest({ path: modalUrl }), + }) + } + return ( + { + ModalService.confirm( + handleModal( + 'Are you sure you want to remove this mailbox rule?', + `/api/ExecRemoveMailboxRule?TenantFilter=${tenantDomain}&ruleId=${ruleId}&ruleName=${ruleName}&userPrincipalName=${userPrincipalName}`, + ), + ) + }} + > + + + ) +} + +export default function UserMailboxRuleList({ userId, userEmail, tenantDomain, className = null }) { + const formatter = (cell) => CellBoolean({ cell }) + const columns = [ + { + selector: (row) => row['Name'], + name: 'Display Name', + sortable: true, + cell: (row) => CellTip(row['Name']), + exportSelector: 'Name', + width: '200px', + }, + { + selector: (row) => row['Description'], + name: 'Description', + sortable: true, + cell: (row) => CellTip(row['Description']), + exportSelector: 'Description', + width: '350px', + }, + { + selector: (row) => row['ForwardTo'], + name: 'Forwards To', + sortable: true, + cell: (row) => CellTip(row['ForwardTo']), + exportSelector: 'ForwardTo', + width: '250px', + }, + { + selector: (row) => row['RedirectTo'], + name: 'Redirect To', + sortable: true, + cell: (row) => CellTip(row['RedirectTo']), + exportSelector: 'RedirectTo', + maxwidth: '250px', + }, + { + selector: (row) => row['CopyToFolder'], + name: 'Copy To Folder', + sortable: true, + cell: (row) => CellTip(row['CopyToFolder']), + exportSelector: 'CopyToFolder', + maxwidth: '200px', + }, + { + selector: (row) => row['MoveToFolder'], + name: 'Move To Folder', + sortable: true, + cell: (row) => CellTip(row['MoveToFolder']), + exportSelector: 'MoveToFolder', + maxwidth: '200px', + }, + { + selector: (row) => row['DeleteMessage'], + name: 'Delete Message', + sortable: true, + cell: cellBooleanFormatter({ colourless: true }), + formatter, + exportSelector: 'DeleteMessage', + width: '200px', + }, + { + name: 'Action', + maxWidth: '100px', + cell: (row) => DeleteMailboxRuleButton(tenantDomain, row['Identity'], userEmail, row['Name']), + }, + ] + return ( + + ) +} + +UserMailboxRuleList.propTypes = { + userId: PropTypes.string.isRequired, + userEmail: PropTypes.string, + tenantDomain: PropTypes.string.isRequired, + className: PropTypes.string, +} diff --git a/src/views/identity/administration/ViewBEC.jsx b/src/views/identity/administration/ViewBEC.jsx new file mode 100644 index 000000000000..9a4338d79b88 --- /dev/null +++ b/src/views/identity/administration/ViewBEC.jsx @@ -0,0 +1,460 @@ +import React, { useEffect } from 'react' +import { CButton, CCallout, CLink, CCardTitle, CSpinner } from '@coreui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faCheckCircle, + faRedo, + faTimesCircle, + faLaptop, + faKey, + faForward, + faUsers, + faAsterisk, + faIdBadge, + faWindowRestore, + faSignInAlt, +} from '@fortawesome/free-solid-svg-icons' +import { useLazyExecBecCheckQuery } from 'src/store/api/users' +import useQuery from 'src/hooks/useQuery' +import { CippTable } from 'src/components/tables' +import { useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { CippContentCard, CippMasonry, CippMasonryItem, CippPage } from 'src/components/layout' +import 'react-loading-skeleton/dist/skeleton.css' +import Skeleton from 'react-loading-skeleton' +import useConfirmModal from 'src/hooks/useConfirmModal' +import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' + +const ViewBec = () => { + let query = useQuery() + const userId = query.get('userId') + const userName = query.get('ID') + const tenantDomain = query.get('tenantDomain') + const [execBecRemediate, execRemediateResults] = useLazyGenericPostRequestQuery() + const [execBecView, results] = useLazyExecBecCheckQuery() + const { data: alerts = {}, isFetching, error, isSuccess } = results + useEffect(() => { + execBecView({ tenantFilter: tenantDomain, userId: userId, userName: userName }) + }, [execBecView, tenantDomain, userId, userName]) + + const deviceColumns = [ + { + name: 'Device Model', + selector: (row) => row['DeviceModel'], + sortable: true, + }, + { + name: 'First Sync Time', + selector: (row) => row['FirstSyncTime'], + sortable: true, + }, + { + name: 'Device User Agent', + selector: (row) => row['DeviceUserAgent'], + sortable: true, + }, + ] + + const rulesColumns = [ + { + name: 'Creator IP', + selector: (row) => row['ClientIP'], + sortable: true, + grow: 0, + }, + { + name: 'Rule Name', + selector: (row) => row['RuleName'], + sortable: true, + }, + { + name: 'Rule Conditions', + selector: (row) => row['RuleCondition'], + sortable: true, + }, + { + name: 'Created on', + selector: (row) => row['CreationTime'], + sortable: true, + }, + { + name: 'Created for', + selector: (row) => row['UserId'], + sortable: true, + }, + ] + + const logonColumns = [ + { + name: 'App', + selector: (row) => row['appDisplayName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Date Time', + selector: (row) => row['createdDateTime'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Error code', + selector: (row) => row.id, + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Details', + selector: (row) => row.Status, + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'IP', + selector: (row) => row.IPAddress, + sortable: true, + cell: cellGenericFormatter(), + }, + ] + + const mailboxlogonColumns = [ + { + name: 'IP', + selector: (row) => row['IPAddress'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'User', + selector: (row) => row['userPrincipalName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Application', + selector: (row) => row['AppDisplayName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Result', + selector: (row) => row['Status'], + sortable: true, + cell: cellGenericFormatter(), + }, + ] + const newUserColumns = [ + { + name: 'DisplayName', + selector: (row) => row['displayName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Username', + selector: (row) => row['userPrincipalName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Date', + selector: (row) => row['createdDateTime'], + sortable: true, + cell: cellGenericFormatter(), + }, + ] + + const passwordColumns = [ + { + name: 'displayName', + selector: (row) => row['displayName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Username', + selector: (row) => row['userPrincipalName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Date', + selector: (row) => row['lastPasswordChangeDateTime'], + sortable: true, + cell: cellGenericFormatter(), + }, + ] + + const permissionColumns = [ + { + name: 'Operation', + selector: (row) => row['Operation'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Executed by', + selector: (row) => row['UserKey'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Executed on', + selector: (row) => row['ObjectId'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Permissions', + selector: (row) => row['Permissions'], + sortable: true, + cell: cellGenericFormatter(), + }, + ] + + const appColumns = [ + { + name: 'Application', + selector: (row) => row['appDisplayName'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Application ID', + selector: (row) => row['appId'], + sortable: true, + cell: cellGenericFormatter(), + }, + { + name: 'Created', + selector: (row) => row['createdDateTime'], + sortable: true, + cell: cellGenericFormatter(), + }, + ] + const handleReMediate = useConfirmModal({ + body:
    Are you sure you want to remediate this user?
    , + onConfirm: () => { + execBecRemediate({ + path: '/api/execBecRemediate', + values: { + userId: userId, + tenantFilter: tenantDomain, + userName: userName, + }, + }) + }, + }) + return ( + + + + Business Email Compromise Overview - {userName}} + button={ + + execBecView({ + tenantFilter: tenantDomain, + userId: userId, + userName: userName, + overwrite: true, + }) + } + disabled={isFetching} + > + {!isFetching && } + Refresh Data + + } + > + + Loading Data: {isFetching && } + {!isFetching && error && } + {isSuccess && ( + <> + + Data has been extracted at {alerts.ExtractedAt} + + )} + +

    + Use this information as a guide to check if a tenant or e-mail address might have been + compromised. All data is retrieved from the last 7 days of logs. +

    +

    + If you need more extensive information, run the{' '} + HAWK tool to investigate further + if you believe this user to be compromised. +

    +

    + Hit the button below to execute the following tasks: +

  • Block user signin
  • +
  • Reset user password
  • +
  • Disconnect all current sessions
  • +
  • Disable all inbox rules for the user
  • +

    + handleReMediate()}>Remediate User + {!execRemediateResults.isSuccess && execRemediateResults.isError && ( + Error. Could not remediate user + )} + {execRemediateResults.isFetching && ( + + + + )} + {execRemediateResults.isSuccess && ( + + {execRemediateResults.data?.Results.map((item, idx) => { + return
  • {item}
  • + })} +
    + )} +
    +
    + + + {isFetching && } + {isSuccess && ( + + )} + + + + + {isFetching && } + + {isSuccess && ( + + )} + + + + + {isFetching && } + {isSuccess && ( + + )} + + + + + {isFetching && } + + {isSuccess && ( + + )} + + + + + {isFetching && } + + {isSuccess && ( + + )} + + + + + {isFetching && } + + {isSuccess && ( + + )} + + + + + {isFetching && } + + {isSuccess && ( + + )} + + + + + {isFetching && } + + {isSuccess && ( + + )} + + +
    +
    + ) +} + +export default ViewBec diff --git a/src/views/identity/administration/ViewUser.jsx b/src/views/identity/administration/ViewUser.jsx new file mode 100644 index 000000000000..a7530eacfbc2 --- /dev/null +++ b/src/views/identity/administration/ViewUser.jsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react' +import { CSpinner } from '@coreui/react' +import PropTypes from 'prop-types' +import useQuery from 'src/hooks/useQuery' +import { useDispatch } from 'react-redux' +import { CippPage, CippMasonry, CippMasonryItem } from 'src/components/layout' +import { ModalService } from 'src/components/utilities' +import UserDevices from 'src/views/identity/administration/UserDevices' +import UserDetails from 'src/views/identity/administration/UserDetails' +import UserLastLoginDetails from 'src/views/identity/administration/UserLastLoginDetails' +import UserCAPs from 'src/views/identity/administration/UserCAPs' +import UserActions from 'src/views/identity/administration/UserActions' +import UserOneDriveUsage from 'src/views/identity/administration/UserOneDriveUsage' +import User365Management from 'src/views/identity/administration/User365Management' +import UserEmailDetails from 'src/views/identity/administration/UserEmailDetails' +import UserEmailUsage from 'src/views/identity/administration/UserEmailUsage' +import UserEmailSettings from 'src/views/identity/administration/UserEmailSettings' +import UserEmailPermissions from 'src/views/identity/administration/UserEmailPermissions' +import UserGroups from 'src/views/identity/administration/UserGroups' +import UserSigninLogs from 'src/views/identity/administration/UserSigninLogs' +import UserMailboxRuleList from 'src/views/identity/administration/UserMailboxRuleList' +import { useListUserQuery } from 'src/store/api/users' + +const ViewUser = (props) => { + const dispatch = useDispatch() + let query = useQuery() + const userId = query.get('userId') + const tenantDomain = query.get('tenantDomain') + const userEmail = query.get('userEmail') + const [queryError, setQueryError] = useState(false) + + const { + data: user = {}, + isFetching: userFetching, + error: userError, + } = useListUserQuery({ tenantDomain, userId }) + + useEffect(() => { + if (!userId || !tenantDomain) { + ModalService.open({ + body: 'Error invalid request, could not load requested user.', + title: 'Invalid Request', + }) + setQueryError(true) + } + }, [tenantDomain, userId, dispatch]) + + return ( + + {userFetching && } + {!userFetching && userError && Error loading user} + {!queryError && !userFetching && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* // TODO: Add support for displaying the result of the delete operation. Currently, the delete operation is performed but the result is not displayed anywhere but the networking tab of the dev tools in the browser. + All API code is in place and should return the needed HTTP status information. + Possibly even remove the row in the table if the delete operation was successful? -Bobby */} + + + + )} + + ) +} + +ViewUser.propTypes = { + params: PropTypes.object, + location: PropTypes.object, +} + +export default ViewUser diff --git a/src/views/tenant/backup/CreateBackup.jsx b/src/views/tenant/backup/CreateBackup.jsx new file mode 100644 index 000000000000..c4dbf63ba81c --- /dev/null +++ b/src/views/tenant/backup/CreateBackup.jsx @@ -0,0 +1,271 @@ +import React, { useState } from 'react' +import { CButton, CCallout, CCol, CForm, CRow, CSpinner, CTooltip } from '@coreui/react' +import { useSelector } from 'react-redux' +import { Field, Form } from 'react-final-form' +import { RFFCFormSwitch } from 'src/components/forms' +import { + useGenericGetRequestQuery, + useLazyGenericGetRequestQuery, + useLazyGenericPostRequestQuery, +} from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleNotch, faEdit, faEye } from '@fortawesome/free-solid-svg-icons' +import { CippPage, CippPageList } from 'src/components/layout' +import 'react-datepicker/dist/react-datepicker.css' +import { ModalService, TenantSelector } from 'src/components/utilities' +import arrayMutators from 'final-form-arrays' +import { useListConditionalAccessPoliciesQuery } from 'src/store/api/tenants' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' +import { CellTip, cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { cellBadgeFormatter, cellDateFormatter } from 'src/components/tables' + +const CreateBackup = () => { + const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery() + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const [refreshState, setRefreshState] = useState(false) + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const onSubmit = (values) => { + const startDate = new Date() + startDate.setHours(0, 0, 0, 0) + const unixTime = Math.floor(startDate.getTime() / 1000) - 45 + const shippedValues = { + TenantFilter: tenantDomain, + Name: `CIPP Backup ${tenantDomain}`, + Command: { value: `New-CIPPBackup` }, + Parameters: { backupType: 'Scheduled', ScheduledBackupValues: { ...values } }, + ScheduledTime: unixTime, + Recurrence: { value: '1d' }, + } + genericPostRequest({ + path: '/api/AddScheduledItem?hidden=true&DisallowDuplicateName=true', + values: shippedValues, + }).then((res) => { + setRefreshState(res.requestId) + }) + } + const Offcanvas = (row, rowIndex, formatExtraData) => { + const handleDeleteSchedule = (apiurl, message) => { + ModalService.confirm({ + title: 'Confirm', + body:
    {message}
    , + onConfirm: () => + ExecuteGetRequest({ path: apiurl }).then((res) => { + setRefreshState(res.requestId) + }), + confirmLabel: 'Continue', + cancelLabel: 'Cancel', + }) + } + let jsonResults + try { + jsonResults = JSON.parse(row.Results) + } catch (error) { + jsonResults = row.Results + } + + return ( + <> + + + handleDeleteSchedule( + `/api/RemoveScheduledItem?&ID=${row.RowKey}`, + 'Do you want to delete this job?', + ) + } + size="sm" + variant="ghost" + color="danger" + > + + + + + ) + } + const columns = [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + cell: (row) => CellTip(row['Tenant']), + exportSelector: 'Tenant', + }, + { + name: 'Task State', + selector: (row) => row['TaskState'], + sortable: true, + cell: cellBadgeFormatter(), + exportSelector: 'TaskState', + }, + { + name: 'Last executed time', + selector: (row) => row['ExecutedTime'], + sortable: true, + cell: cellDateFormatter({ format: 'relative' }), + exportSelector: 'ExecutedTime', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '100px', + }, + ] + + const { + data: users = [], + isFetching: usersIsFetching, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenantDomain, + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName,accountEnabled', + $count: true, + $top: 999, + $orderby: 'displayName', + }, + }) + + const { + data: caPolicies = [], + isFetching: caIsFetching, + error: caError, + } = useListConditionalAccessPoliciesQuery({ domain: tenantDomain }) + + return ( + + <> + + + + Create Backup Schedule + {postResults.isFetching && ( + + )} + + } + title="Add backup Schedule" + icon={faEdit} + > + { + return ( + +

    + Backups are stored in CIPPs storage and can be restored using the CIPP + Restore Backup Wizard. Backups run daily or on demand by clicking the backup + now button. +

    + + + + {(props) => } + + + +
    +
    + + +

    Identity

    + + +

    Conditional Access

    + +

    Intune

    + + + +

    Email Security

    + + +

    CIPP

    + + + +
    +
    + {postResults.isSuccess && ( + +
  • {postResults.data.Results}
  • +
    + )} + {getResults.isFetching && ( + + Loading + + )} + {getResults.isSuccess && ( + {getResults.data?.Results} + )} + {getResults.isError && ( + + Could not connect to API: {getResults.error.message} + + )} +
    + ) + }} + /> +
    +
    + + + + +
    + +
    + ) +} + +export default CreateBackup diff --git a/src/views/tenant/backup/RestoreBackup.jsx b/src/views/tenant/backup/RestoreBackup.jsx new file mode 100644 index 000000000000..4091debf2881 --- /dev/null +++ b/src/views/tenant/backup/RestoreBackup.jsx @@ -0,0 +1,242 @@ +import React, { useState } from 'react' +import { CCallout, CCol, CListGroup, CListGroupItem, CRow, CSpinner, CTooltip } from '@coreui/react' +import { Field, FormSpy } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExclamationTriangle, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons' +import { useSelector } from 'react-redux' +import { CippCallout, CippWizard } from 'src/components/layout' +import PropTypes from 'prop-types' +import { Condition, RFFCFormSwitch, RFFSelectSearch } from 'src/components/forms' +import { TenantSelector } from 'src/components/utilities' +import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' +import 'react-datepicker/dist/react-datepicker.css' + +const Error = ({ name }) => ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +const OffboardingWizard = () => { + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const { + data: currentBackups = [], + isFetching: currentBackupsIsFetching, + error: currentBackupsError, + } = useGenericGetRequestQuery({ + path: `/api/ExecListBackup?TenantFilter=${tenantDomain}&Type=Scheduled`, + }) + + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const handleSubmit = (values) => { + const startDate = new Date() + const unixTime = Math.floor(startDate.getTime() / 1000) - 45 + const shippedValues = { + TenantFilter: tenantDomain, + Name: `CIPP Restore ${tenantDomain}`, + Command: { value: `New-CIPPRestore` }, + Parameters: { Type: 'Scheduled', RestoreValues: { ...values } }, + ScheduledTime: unixTime, + PostExecution: { + Webhook: values.webhook, + Email: values.email, + PSA: values.psa, + }, + } + genericPostRequest({ path: '/api/AddScheduledItem', values: shippedValues }).then((res) => {}) + } + + return ( + + +
    +

    Step 1

    +
    Choose a tenant
    +
    +
    + {(props) => } +
    +
    + +
    +

    Step 2

    +
    Select the backup to restore
    +
    +
    +
    + ({ + value: backup.RowKey, + name: `${backup.RowKey}`, + }))} + placeholder={!currentBackupsIsFetching ? 'Select a backup' : 'Loading...'} + name="backup" + /> + {currentBackupsError && Failed to load list of Current Backups} +
    +
    +
    + +
    +

    Step 3

    +
    Choose restore options
    +
    +
    +
    + + +

    Identity

    + + +

    Conditional Access

    + +

    Intune

    + + + +
    + +

    Email Security

    + + +

    CIPP

    + + + +
    +
    +
    + + + + + + +
    Warning
    +

    + Overwriting existing entries will remove the current settings and replace them + with the backup settings. If you have selected to restore users, all properties + will be overwritten with the backup settings. +

    + +

    + To prevent and skip already existing entries, deselect the setting from the list + above, or disable overwrite. +

    +
    +
    +
    +
    + + + + + + + + +
    +
    +
    + +
    +

    Step 4

    +
    Confirm and apply
    +
    +
    +
    + {postResults.isFetching && ( + + Loading + + )} + {postResults.isSuccess && {postResults.data.Results}} + {!postResults.isSuccess && ( + + {/* eslint-disable react/prop-types */} + {(props) => ( + <> + + + + +
    Selected Tenant:
    + {tenantDomain} +
    + +
    Selected Backup:
    + {props.values.backup.value} +
    +
    +
    +
    +
    + + + + + Overwrite existing configuration + + + + Send results to Webhook + + + + Send results to E-Mail + + + + Send results to PSA + + + + + + + )} +
    + )} +
    +
    +
    +
    + ) +} + +export default OffboardingWizard diff --git a/src/views/tenant/standards/BestPracticeAnalyser.jsx b/src/views/tenant/standards/BestPracticeAnalyser.jsx new file mode 100644 index 000000000000..692c39478591 --- /dev/null +++ b/src/views/tenant/standards/BestPracticeAnalyser.jsx @@ -0,0 +1,471 @@ +import React, { useEffect, useState } from 'react' +import { + CBadge, + CButton, + CCard, + CCardBody, + CCardHeader, + CCardText, + CCardTitle, + CCol, + CCollapse, + CForm, + CRow, + CSpinner, +} from '@coreui/react' +import useQuery from 'src/hooks/useQuery' +import PropTypes from 'prop-types' +import { Field, Form, FormSpy } from 'react-final-form' +import { RFFCFormInput, RFFCFormSelect } from 'src/components/forms' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + faChevronRight, + faChevronDown, + faSearch, + faExclamationTriangle, + faCheck, + faCross, + faTimes, + faSync, + faExclamation, +} from '@fortawesome/free-solid-svg-icons' +import { CippTable, cellBooleanFormatter } from 'src/components/tables' +import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { CippPage } from 'src/components/layout/CippPage' +import { useGenericGetRequestQuery, useLazyGenericGetRequestQuery } from 'src/store/api/app' +import { OnChange } from 'react-final-form-listeners' +import { queryString } from 'src/helpers' +import { CellTip, cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { useExecBestPracticeAnalyserMutation } from 'src/store/api/reports' +import { ModalService } from 'src/components/utilities' +import { cellTableFormatter } from 'src/components/tables/CellTable' +import { cellMathFormatter } from 'src/components/tables/CellMathFormatter' + +const RefreshAction = ({ singleTenant = false, refreshFunction = null }) => { + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const [execBestPracticeAnalyser, { isLoading, isSuccess, error }] = + useLazyGenericGetRequestQuery() + var params = {} + if (singleTenant) { + params['TenantFilter'] = tenantDomain + } + const showModal = () => + ModalService.confirm({ + body: ( +
    + Are you sure you want to force the Best Practice Analysis to run? This will slow down + normal usage considerably.
    + Please note: this runs at 3:00 AM UTC automatically every day. +
    + ), + onConfirm: () => + execBestPracticeAnalyser({ + path: 'api/ExecBPA', + params: params, + }), + }) + + return ( + <> + + {isLoading && } + {error && } + {isSuccess && } + {(singleTenant && 'Refresh Tenant Data') || 'Force Refresh All Data'} + + {refreshFunction !== null && ( + { + refreshFunction((Math.random() + 1).toString(36).substring(7)) + }} + className="m-1" + size="sm" + > + + + )} + + ) +} +RefreshAction.propTypes = { + singleTenant: PropTypes.bool, + refreshFunction: PropTypes.func, +} + +const getsubcolumns = (data) => { + const flatObj = data && data.length > 0 ? data : [{ data: 'No Data Found' }] + const QueryColumns = [] + + if (flatObj[0]) { + Object.keys(flatObj[0]).map((key) => { + QueryColumns.push({ + name: key, + selector: (row) => row[key], // Accessing the property using the key + sortable: true, + exportSelector: key, + cell: cellGenericFormatter(), + }) + }) + } + return QueryColumns +} +const getNestedValue = (obj, path) => { + if (!path) return undefined + return path.split('.').reduce((acc, part) => { + // Check for an array marker + const match = part.match(/(.*?)\[(\d+)\]/) + if (match) { + const propName = match[1] + const index = parseInt(match[2], 10) + return acc[propName] ? acc[propName][index] : undefined + } + // If no array marker, simply return the property value + return acc ? acc[part] : undefined + }, obj) +} +const BestPracticeAnalyser = () => { + const [reportTemplate, setReportTemplate] = useState('CIPP Best Practices v1.0 - Table view') + const [refreshValue, setRefreshValue] = useState('') + const { data: templates = [], isLoading: templatesfetch } = useGenericGetRequestQuery({ + path: 'api/listBPATemplates', + }) + let navigate = useNavigate() + const tenant = useSelector((state) => state.app.currentTenant) + let query = useQuery() + const Report = query.get('Report') + const SearchNow = query.get('SearchNow') + const [visibleA, setVisibleA] = useState(true) + const handleSubmit = async (values) => { + setVisibleA(false) + setReportTemplate(values.reportTemplate) + const shippedValues = { + SearchNow: true, + Report: reportTemplate, + tenantFilter: tenant.customerId, + random: (Math.random() + 1).toString(36).substring(7), + } + var queryString = Object.keys(shippedValues) + .map((key) => key + '=' + shippedValues[key]) + .join('&') + + navigate(`?${queryString}`) + } + const [execGraphRequest, graphrequest] = useLazyGenericGetRequestQuery() + const QueryColumns = { + set: false, + data: [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + exportSelector: 'Tenant', + cell: (row) => CellTip(row['Tenant']), + }, + ], + } + const normalizeTableData = (value) => { + if (Array.isArray(value)) { + return value + } else if (value === null) { + return null + } else if (typeof value === 'object') { + return [value] + } else { + return value + } + } + if (graphrequest.isSuccess) { + if (graphrequest.data.length === 0) { + graphrequest.data = [{ data: 'No Data Found' }] + } + const flatObj = graphrequest.data.Columns.length >= 0 ? graphrequest.data.Columns : [] + + flatObj.map((col) => { + if (col === null) { + return + } + // Determine the cell selector based on the 'formatter' property + let cellSelector + if (col.formatter) { + switch (col.formatter) { + case 'bool': + cellSelector = cellBooleanFormatter() + break + case 'reverseBool': + cellSelector = cellBooleanFormatter({ reverse: true }) + break + case 'warnBool': + cellSelector = cellBooleanFormatter({ warning: true }) + break + case 'table': + cellSelector = cellTableFormatter(col.value) + break + case 'math': + cellSelector = cellMathFormatter({ col }) + break + default: + cellSelector = cellGenericFormatter() + break + } + } else { + cellSelector = cellGenericFormatter() + } + + QueryColumns.data.push({ + name: col.name, + selector: (row) => getNestedValue(row, col.value), + sortable: true, + exportSelector: col.value.split('.').join('/'), + cell: cellSelector, // Use the determined cell selector + }) + }) + + QueryColumns.set = true + } + + useEffect(() => { + execGraphRequest({ + path: 'api/listBPA', + params: { + tenantFilter: tenant.customerId, + Report: reportTemplate, + SearchNow: SearchNow, + refresh: refreshValue, + }, + }) + }, [ + Report, + execGraphRequest, + tenant.defaultDomainName, + query, + refreshValue, + reportTemplate, + tenant.customerId, + SearchNow, + ]) + return ( + <> + + + + + + Report Settings + setVisibleA(!visibleA)} + > + + + + + + + + + + { + return ( + + + + ({ + label: template.Name, + value: template.Name, + }))} + /> + + {templatesfetch && } + + + + + + + Retrieve Report + + + + + ) + }} + /> + + + + + +
    + + + + {graphrequest.isUninitialized && Choose a BPA Report to get started.} + {graphrequest.isFetching && } + {graphrequest.isSuccess && QueryColumns.set && graphrequest.data.Style == 'Table' && ( + + + Best Practice Report + + + , + ], + }} + /> + + + )} + {graphrequest.isSuccess && QueryColumns.set && graphrequest.data.Style == 'Tenant' && ( + <> + +
    + +
    + {graphrequest.data?.Data[0] && + Object.keys(graphrequest.data.Data[0]).length === 0 ? ( + + + Best Practice Report + + + + No Data Found for this tenant. Please refresh the tenant data. + + + + ) : ( + graphrequest.data.Columns.map((info, idx) => ( + + + + {info.name} + + + + {info.formatter === 'bool' && ( + + + {graphrequest.data.Data[0][info.value] ? 'Yes' : 'No'} + + )} + {info.formatter === 'reverseBool' && ( + + + {graphrequest.data.Data[0][info.value] ? 'No' : 'Yes'} + + )} + {info.formatter === 'warnBool' && ( + + + {graphrequest.data.Data[0][info.value] ? 'Yes' : 'No'} + + )} + + {info.formatter === 'table' && ( + <> + + + )} + + {info.formatter === 'number' && ( +

    + {getNestedValue(graphrequest.data.Data[0], info.value)} +

    + )} +
    + + {info.desc} + +
    +
    +
    + )) + )} +
    + + )} +
    +
    +
    + + ) +} + +export default BestPracticeAnalyser diff --git a/version_latest.txt b/version_latest.txt index 5e39348ef037..2e0b229ff633 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1,5 @@ -99.99.99 \ No newline at end of file +<<<<<<< HEAD +6.4.1 +======= +99.99.99 +>>>>>>> 81ee51ffa0576b02dc60ee5600a44cec3cde3e97