diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 0c6b95cbf595..1e0e6afd8faa 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -31,7 +31,12 @@ body: attributes: label: Environment data description: > - Please let us know your environment information - e.g. hosted version, Azure, local Dev version. + Please let us know your environment information. This must follow this format or the ticket will be closed: + Sponsored / Non-sponsored instance + Front end version number: + Back end version number: + Tried Tenant Cache Clear: true/false + Tried Token Cache Clear: true/false render: PowerShell validations: required: true diff --git a/.github/workflows/Comment_on_Issues.yml b/.github/workflows/Comment_on_Issues.yml index 6bd12f6a005b..80574ef5d235 100644 --- a/.github/workflows/Comment_on_Issues.yml +++ b/.github/workflows/Comment_on_Issues.yml @@ -16,7 +16,7 @@ jobs: with: issue-number: ${{ github.event.issue.number }} body: | - Thank you for creating a bug. Please make sure your bug is indeed a unique case by checking current and past issues, and reading the complete documentation at https://kelvintegelaar.github.io/CIPP + Thank you for creating a bug. Please make sure your bug is indeed a unique case by checking current and past issues, and reading the complete documentation at https://docs.cipp.app/ If your bug is a known documentation issue, it will be closed without notice by a contributor. To confirm that this is not a bug found in the documentation, please copy and paste the following comment: "I confirm that I have checked the documentation thoroughly and believe this to be an actual bug.". Without confirming, your report will be closed in 24 hours. If you'd like this bug to be assigned to you, please comment "I would like to work on this please!". diff --git a/.vscode/launch.json b/.vscode/launch.json index fcd6fc8d6c56..e8f4c409c8fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,13 @@ { "version": "0.2.0", "configurations": [ + { + "name": "PowerShell: Execute current file", + "type": "PowerShell", + "request": "launch", + "script": "${file}", + "cwd": "${file}" + }, { "command": "npm run start-api", "name": "Run emulator", diff --git a/README.md b/README.md index 9e42be1b45e1..677617c1ae29 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,10 @@ ![CyberDrain Light](github_assets/img/CIPP.png#gh-dark-mode-only) ![CyberDrain Dark](github_assets/img/CIPP-Light.png#gh-light-mode-only) -
- -[![GitHub Latest Release](https://img.shields.io/github/v/release/KelvinTegelaar/CIPP?label=Latest%20Release&style=for-the-badge)](https://github.com/KelvinTegelaar/CIPP/releases) -![CodeQL Security Analysis Status](https://img.shields.io/github/workflow/status/KelvinTegelaar/CIPP/CodeQL?label=CodeQL%20Security&style=for-the-badge) -[![GitHub Enhancement Requests](https://img.shields.io/github/issues/KelvinTegelaar/CIPP/enhancement?label=Enhancement%20Requests&style=for-the-badge)](https://github.com/KelvinTegelaar/CIPP/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) -[![GitHub Bugs](https://img.shields.io/github/issues/KelvinTegelaar/CIPP/bug?label=Bugs&style=for-the-badge)](https://github.com/KelvinTegelaar/CIPP/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Abug) -[![Discord](https://img.shields.io/discord/905453405936447518?label=Discord&style=for-the-badge)](https://discord.com/invite/cyberdrain) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/KelvinTegelaar?label=Public%20Sponsors&style=for-the-badge)](https://github.com/sponsors/KelvinTegelaar) - -
-

Sponsored by

![OIT](github_assets/img/oitpsonsor_light.png)     -![Genuine Technology Services](github_assets/img/Genuine-logo-vertical-light.png)     ![Immybot](github_assets/img/Immybot.png)     ![NinjaOne](github_assets/img/NinjaOne-Light.png#gh-dark-mode-only) ![NinjaOne](github_assets/img/NinjaOne-Dark.png#gh-light-mode-only)     @@ -31,143 +19,4 @@ The CyberDrain Improved Partner Portal is a portal to help manage administration This project is a way to help you with administration, with user management, and deploying your own preferred standards. It's not a replacement for security tools, or a way to cut costs on specific subscriptions. The tool should assist you in removing the gripes with standard partner management and save you several hours per engineer per month. -# Deployment and Getting Started - -If you want to self-host, check out the installation manual [here](https://cipp.app/GettingStarted/Installation/). You will need some knowledge of Static Web Apps, Azure Functions, and Azure Keyvault - -# Why are you making this? - -I'm kind of done waiting for vendors to catch up to what we actually need. All RMM vendors are dramaticaly slow adopting cloud management. Microsoft themselves don't understand the Managed services markets, there are vendors that have tried jumping into the gap but either have unreasonable fees, weird constructions, require Global Admins without MFA, or just don't innovate at a pace that is required of cloud services right now. - -I'm also annoyed the untransparent behaviour that many companies in our market are showing. Most are claiming that working with the Microsoft Partner APIs is difficult, and requires a very heavy development team. I'm a guy that had no webdesign knowledge before this and created the first release of this app in 3 weekends. Vendors that claim high difficulty or issues with integration are simply not giving this _any_ priority. - -I was recently on a call with one of my friends and he said he was changing the world. That insipred me to change the world just a little bit too. :) I'm hoping that this is one of the tools that make you smile. - -# What's the pricing? - -This project is **FREE** but we do have a **Sponsorware** component. The sponsorware structure for this project is pretty simple; the code is available to everyone and free to use. You will need some technical know-how to put it all together. Sponsors receive the following benefits - -### For users of the project that sponsor: - -- The project will be hosted for you. -- The hosted version will always be the latest release and automatically updated. -- You'll also receive a staging environment with the latest (nightly/beta) build, to see new features before anyone else. -- You will receive priority on support issues reported on GitHub. -- You will be able to make 1 priortized feature request per month. - -Sponsorship allows me to sink some more time into this project and keep it free, so please consider it. :) - -### For company sponsors, depending on sponsor level you can get the following benefits; - -- Your company logo will be featured on this readme page at the top. -- Your company logo will be featured on https://cyberdrain.com -- A small version of your company logo with a link to your homepage will be on the footer, each user will see this on each page. - -# How does it look?! - -Check out the GIFs below to see how some of the workflows work. - - - - - - - - - -# What is the functionality? - -The current build functionality is described below, also check out our Changelog in the documentation folder, as the tool has a very rapid development schedule the list below might be out of date. - -## Identity Management - -- Manage M365 users - - List users, email addreses, and licenses. - - View & Edit user settings - - Research if user has been compromised - - Send user an MFA push to confirm their identity - - Convert a user to a shared mailbox - - Block signin, reset passwords - - Delete users -- Manage M365 groups - - List all M365 groups, group types, and e-mail addresses. - - Edit members and group owners -- Offboard users via an easy wizard - - Remove user licenses - - Convert user to shared mailbox - - Disable user sign-in - - Reset user password - - Remove user from all groups - - Hide user from address list - - Set Out of Office - - Give other user access to mailbox, and OneDrive - -## Tenants - -- Manage M365 tenants - - List all tenants and quick-links to the most user portals using delegated access. - - Edit Partner tenant names and default domain for your CSP partner environment - - List tenant conditional access policies - - Apply standard configuration to tenant on a repeat schedule. - - Execute a best practice analysis daily and report on best practice settings - - Analyse current domains, and domains outside of M365 for optimal security settings - - List alerts for tenants - -## Endpoint Management - -- Applications - - List all applications in tenants - - List installation status of a specific application per device - - Add Office Apps to multiple tenants - - Add/Remove Chocolatey Apps to multiple tenants - - Assign Apps to All Devices or All Users - - Report on installation status -- Autopilot - - Manage and create autopilot devices, profiles, status pages. -- Intune - - List Intune policies - - Apply Intune Policies - - Add Intune Policy Templates to deploy over multiple tenants - -## Teams & SharePoint - -- List OneDrive, Teams, and SharePoint usage -- View current Teams, installed applications, Team owners, members, and channels -- Add and edit Teams, members, owners and apps. -- Tenant Alerting - -## Exchange - -- View mailboxes and contacts -- View user mobile devices -- Convert mailboxes to shared or user mailboxes -- Report mailbox statistics, client access settings -- Perform message traces -- Change and view phishing policies. - -## Application settings - -- Use multiple user levels (readonly, editor, admin) to manage access -- Allow excluding of tenants -- Send automated alert emails to webhook or e-mail - -# Security - -Authentication is handled by Azure AD using static web apps security. This means the API is only reachable for authenticated users you've invited. For most of the security info related to that check out our staticwebapp.config.json and/or the doc pages on static web apps. Do you see something that might be a security risk, even the smallest? report it and we will handle it asap. Check out our security reporting options [here](https://github.com/KelvinTegelaar/CIPP/security) - -# Contributions - -Feel free to send pull requests or fill out issues when you encounter them, sponsors get a priority on issues and bugs. I'm also completely open to adding direct maintainers/contributors and working together. - -If you decide to contribute; remember that keeping the portal fast is a key component. CIPP is supposed to go brrrrr, any improvements that help with speed are welcomed. - -## Special thanks - -I'd like to give special thanks to the people that made this project possible; - -- [Kyle Hanslovan](https://huntress.com) -- [Ray Orsini](https://oit.co) -- The Team at [MSP.zone/MSP'R'Us](https://msp.zone) -- Gavin Stone at [MSPGeek](https://mspgeek.org) -- MSP2.0 for helping with some visual input. -- Scott, Chris, Jon, and others that helped me with some of the internals of the app. +for more information, we recommend checking out our website [here](https://cipp.app) diff --git a/deployment/AzureDeploymentTemplate.json b/deployment/AzureDeploymentTemplate.json index 620700934567..bc7eb0ba9efe 100644 --- a/deployment/AzureDeploymentTemplate.json +++ b/deployment/AzureDeploymentTemplate.json @@ -115,18 +115,6 @@ "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" ] - }, - { - "type": "secrets", - "name": "exchangerefreshtoken", - "apiVersion": "2015-06-01", - "properties": { - "contentType": "text/plain", - "value": "ExchangeRefreshToken" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" - ] } ], "dependsOn": ["[resourceId('Microsoft.Web/sites', variables('funcAppName'))]"] @@ -178,10 +166,6 @@ "name": "RefreshToken", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/RefreshToken)')]" }, - { - "name": "ExchangeRefreshtoken", - "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/ExchangeRefreshToken)')]" - }, { "name": "TenantID", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/tenantid)')]" diff --git a/deployment/AzureDeploymentTemplate_regionoptions.json b/deployment/AzureDeploymentTemplate_regionoptions.json index c187bb7dc914..11fccba7350c 100644 --- a/deployment/AzureDeploymentTemplate_regionoptions.json +++ b/deployment/AzureDeploymentTemplate_regionoptions.json @@ -115,18 +115,6 @@ "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" ] - }, - { - "type": "secrets", - "name": "exchangerefreshtoken", - "apiVersion": "2015-06-01", - "properties": { - "contentType": "text/plain", - "value": "ExchangeRefreshToken" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" - ] } ], "dependsOn": ["[resourceId('Microsoft.Web/sites', variables('funcAppName'))]"] @@ -178,10 +166,6 @@ "name": "RefreshToken", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/RefreshToken)')]" }, - { - "name": "ExchangeRefreshtoken", - "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/ExchangeRefreshToken)')]" - }, { "name": "TenantID", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/tenantid)')]" diff --git a/deployment/DevAzureDeploymentTemplate.json b/deployment/DevAzureDeploymentTemplate.json index a653cebc66dd..dee70ee11871 100644 --- a/deployment/DevAzureDeploymentTemplate.json +++ b/deployment/DevAzureDeploymentTemplate.json @@ -37,13 +37,6 @@ "description": "The Refresh token for your Secure Application Model." } }, - "ExchangeRefreshToken": { - "defaultValue": "LongRefreshtoken", - "type": "string", - "metadata": { - "description": "The Exchange Refresh token for your Secure Application Model." - } - }, "GithubRepository": { "defaultValue": "https://github.com/KelvinTegelaar/CIPP", "type": "string", @@ -150,18 +143,6 @@ "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" ] - }, - { - "type": "secrets", - "name": "exchangerefreshtoken", - "apiVersion": "2015-06-01", - "properties": { - "contentType": "text/plain", - "value": "[parameters('exchangerefreshtoken')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" - ] } ], "dependsOn": ["[resourceId('Microsoft.Web/sites', variables('funcAppName'))]"] @@ -213,10 +194,6 @@ "name": "RefreshToken", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/RefreshToken)')]" }, - { - "name": "ExchangeRefreshtoken", - "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/ExchangeRefreshToken)')]" - }, { "name": "TenantID", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/tenantid)')]" diff --git a/deployment/DevAzureDeploymentTemplate_regionoptions.json b/deployment/DevAzureDeploymentTemplate_regionoptions.json index 7af2b9af4905..301d50d5437a 100644 --- a/deployment/DevAzureDeploymentTemplate_regionoptions.json +++ b/deployment/DevAzureDeploymentTemplate_regionoptions.json @@ -37,13 +37,6 @@ "description": "The Refresh token for your Secure Application Model." } }, - "ExchangeRefreshToken": { - "defaultValue": "LongRefreshtoken", - "type": "string", - "metadata": { - "description": "The Exchange Refresh token for your Secure Application Model." - } - }, "GithubRepository": { "defaultValue": "https://github.com/KelvinTegelaar/CIPP", "type": "string", @@ -150,18 +143,6 @@ "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" ] - }, - { - "type": "secrets", - "name": "exchangerefreshtoken", - "apiVersion": "2015-06-01", - "properties": { - "contentType": "text/plain", - "value": "[parameters('exchangerefreshtoken')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('uniqueResourceNameBase'))]" - ] } ], "dependsOn": ["[resourceId('Microsoft.Web/sites', variables('funcAppName'))]"] @@ -213,10 +194,6 @@ "name": "RefreshToken", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/RefreshToken)')]" }, - { - "name": "ExchangeRefreshtoken", - "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/ExchangeRefreshToken)')]" - }, { "name": "TenantID", "value": "[concat('@Microsoft.KeyVault(SecretUri=https://',variables('uniqueResourceNameBase'), '.vault.azure.net/secrets/tenantid)')]" diff --git a/package-lock.json b/package-lock.json index 51ada863cb0a..619b33124282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "enzyme": "^3.11.0", "final-form": "^4.20.4", "fuzzysort": "^1.1.4", + "javascript-time-ago": "^2.5.9", "jspdf": "^2.4.0", "jspdf-autotable": "^3.5.23", "moment": "^2.29.1", @@ -53,6 +54,7 @@ "react-select": "^5.3.0", "react-select-search": "^3.0.8", "react-syntax-highlighter": "^15.4.5", + "react-time-ago": "^7.2.1", "redux": "4.1.1", "redux-persist": "^6.0.0", "simplebar-react": "^2.3.6", @@ -7107,8 +7109,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.9.2", - "license": "MIT", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -8824,7 +8827,8 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "node_modules/global-modules": { "version": "2.0.0", @@ -10063,6 +10067,14 @@ "node": ">=8" } }, + "node_modules/javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "dependencies": { + "relative-time-format": "^1.1.6" + } + }, "node_modules/jest": { "version": "27.5.1", "dev": true, @@ -11851,10 +11863,6 @@ "node": ">=4" } }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "license": "MIT" @@ -15165,6 +15173,26 @@ "react": "17.0.2" } }, + "node_modules/react-time-ago": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.2.1.tgz", + "integrity": "sha512-X5zwJDZHa1fsMwMvh8mrHN31g85s84zMCp+d7YL6IX50kNnr6YMAS2wpt1BmO9OxBV2Ue5J1ptD6JI8Zjd35HA==", + "dependencies": { + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1", + "raf": "^3.4.1" + }, + "peerDependencies": { + "javascript-time-ago": "^2.3.7", + "react": ">=0.16.8", + "react-dom": ">=0.16.8" + } + }, + "node_modules/react-time-ago/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/react-transition-group": { "version": "4.4.2", "license": "BSD-3-Clause", @@ -15502,6 +15530,11 @@ "node": ">= 0.10" } }, + "node_modules/relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" + }, "node_modules/renderkid": { "version": "3.0.0", "dev": true, @@ -17528,8 +17561,9 @@ } }, "node_modules/watchpack": { - "version": "2.3.1", - "license": "MIT", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -17555,32 +17589,33 @@ } }, "node_modules/webpack": { - "version": "5.69.1", - "license": "MIT", + "version": "5.77.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz", + "integrity": "sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", + "acorn": "^8.7.1", "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", + "enhanced-resolve": "^5.10.0", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", + "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", + "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "bin": { @@ -22755,7 +22790,9 @@ "dev": true }, "enhanced-resolve": { - "version": "5.9.2", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -23851,7 +23888,9 @@ } }, "glob-to-regexp": { - "version": "0.4.1" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "global-modules": { "version": "2.0.0", @@ -24582,6 +24621,14 @@ } } }, + "javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "requires": { + "relative-time-format": "^1.1.6" + } + }, "jest": { "version": "27.5.1", "dev": true, @@ -25730,9 +25777,6 @@ "jsesc": { "version": "2.5.2" }, - "json-parse-better-errors": { - "version": "1.0.2" - }, "json-parse-even-better-errors": { "version": "2.3.1" }, @@ -27686,6 +27730,23 @@ "scheduler": "^0.20.2" } }, + "react-time-ago": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.2.1.tgz", + "integrity": "sha512-X5zwJDZHa1fsMwMvh8mrHN31g85s84zMCp+d7YL6IX50kNnr6YMAS2wpt1BmO9OxBV2Ue5J1ptD6JI8Zjd35HA==", + "requires": { + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1", + "raf": "^3.4.1" + }, + "dependencies": { + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + } + } + }, "react-transition-group": { "version": "4.4.2", "requires": { @@ -27913,6 +27974,11 @@ "version": "0.2.7", "dev": true }, + "relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" + }, "renderkid": { "version": "3.0.0", "dev": true, @@ -29235,7 +29301,9 @@ } }, "watchpack": { - "version": "2.3.1", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -29253,31 +29321,33 @@ "dev": true }, "webpack": { - "version": "5.69.1", + "version": "5.77.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz", + "integrity": "sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==", "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", + "acorn": "^8.7.1", "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", + "enhanced-resolve": "^5.10.0", "es-module-lexer": "^0.9.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", + "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", + "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "dependencies": { diff --git a/package.json b/package.json index d9e64244f095..7d4a99103c8f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "enzyme": "^3.11.0", "final-form": "^4.20.4", "fuzzysort": "^1.1.4", + "javascript-time-ago": "^2.5.9", "jspdf": "^2.4.0", "jspdf-autotable": "^3.5.23", "moment": "^2.29.1", @@ -72,6 +73,7 @@ "react-select": "^5.3.0", "react-select-search": "^3.0.8", "react-syntax-highlighter": "^15.4.5", + "react-time-ago": "^7.2.1", "redux": "4.1.1", "redux-persist": "^6.0.0", "simplebar-react": "^2.3.6", diff --git a/public/version_latest.txt b/public/version_latest.txt index a0cd9f0ccb01..3c8ff8c36b50 100644 --- a/public/version_latest.txt +++ b/public/version_latest.txt @@ -1 +1 @@ -3.1.0 \ No newline at end of file +3.5.1 \ No newline at end of file diff --git a/src/_nav.js b/src/_nav.js index 6a8db77a4ceb..f0768034e29a 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -18,6 +18,7 @@ import { faExclamationTriangle, faUserShield, faEnvelope, + faToolbox, } from '@fortawesome/free-solid-svg-icons' const _nav = [ @@ -90,8 +91,8 @@ const _nav = [ }, { component: CNavItem, - name: 'Basic Auth Report', - to: '/identity/reports/basic-auth-report', + name: 'Inactive Users', + to: '/identity/reports/inactive-users-report', }, { component: CNavItem, @@ -134,6 +135,35 @@ const _nav = [ }, ], }, + { + component: CNavGroup, + name: 'Tools', + section: 'Tools', + to: '/tenant/administration', + icon: , + items: [ + { + component: CNavItem, + name: 'Graph Explorer', + to: '/tenant/administration/graph-explorer', + }, + { + component: CNavItem, + name: 'Application Approval', + to: '/tenant/administration/appapproval', + }, + { + component: CNavItem, + name: 'Tenant Lookup', + to: '/tenant/administration/tenantlookup', + }, + { + component: CNavItem, + name: 'Individual Domain Check', + to: '/tenant/standards/individual-domains', + }, + ], + }, { component: CNavGroup, name: 'Standards', @@ -161,11 +191,6 @@ const _nav = [ name: 'Domains Analyser', to: '/tenant/standards/domains-analyser', }, - { - component: CNavItem, - name: 'Individual Domain Check', - to: '/tenant/standards/individual-domains', - }, ], }, { @@ -214,11 +239,6 @@ const _nav = [ to: '/tenant/reports', icon: , items: [ - { - component: CNavItem, - name: 'Graph Explorer', - to: '/tenant/administration/graph-explorer', - }, { component: CNavItem, name: 'Licence Report', @@ -606,6 +626,11 @@ const _nav = [ name: 'Phishing Policies', to: '/email/reports/phishing-policies', }, + { + component: CNavItem, + name: 'Shared Mailbox with Enabled Account', + to: '/email/reports/SharedMailboxEnabledAccount', + }, ], }, { @@ -644,6 +669,16 @@ const _nav = [ to: '/cipp/gdap', icon: , items: [ + { + component: CNavItem, + name: 'Role Wizard', + to: '/tenant/administration/gdap-role-wizard', + }, + { + component: CNavItem, + name: 'GDAP Roles', + to: '/tenant/administration/gdap-roles', + }, { component: CNavItem, name: 'Migration Wizard', @@ -656,8 +691,8 @@ const _nav = [ }, { component: CNavItem, - name: 'Application Approval', - to: '/tenant/administration/appapproval', + name: 'GDAP Relationships', + to: '/tenant/administration/gdap-relationships', }, { component: CNavItem, diff --git a/src/adminRoutes.js b/src/adminRoutes.js index fab00d4080e5..7fb235df0e0e 100644 --- a/src/adminRoutes.js +++ b/src/adminRoutes.js @@ -4,6 +4,11 @@ const Setup = React.lazy(() => import('src/views/cipp/Setup')) const ApplyStandard = React.lazy(() => import('src/views/tenant/standards/ApplyStandard')) const GDAPStatus = React.lazy(() => import('src/views/tenant/administration/ListGDAPQueue')) const GDAP = React.lazy(() => import('src/views/tenant/administration/GDAPWizard')) +const GDAPRoleWizard = React.lazy(() => import('src/views/tenant/administration/GDAPRoleWizard')) +const GDAPRoles = React.lazy(() => import('src/views/tenant/administration/ListGDAPRoles')) +const GDAPRelationships = React.lazy(() => + import('./views/tenant/administration/ListGDAPRelationships'), +) const appapproval = React.lazy(() => import('src/views/cipp/AppApproval')) const adminRoutes = [ @@ -12,6 +17,21 @@ const adminRoutes = [ { path: '/cipp/settings', name: 'Settings', component: CIPPSettings }, { path: '/cipp/setup', name: 'Setup', component: Setup }, { path: '/tenant/administration/gdap', name: 'GDAP Wizard', component: GDAP }, + { + path: '/tenant/administration/gdap-role-wizard', + name: 'GDAP Role Wizard', + component: GDAPRoleWizard, + }, + { + path: '/tenant/administration/gdap-roles', + name: 'GDAP Roles', + component: GDAPRoles, + }, + { + path: '/tenant/administration/gdap-relationships', + name: 'GDAP Relationships', + component: GDAPRelationships, + }, { path: '/tenant/administration/appapproval', name: 'App Approval', component: appapproval }, { path: '/tenant/administration/gdap-status', name: 'GDAP Status', component: GDAPStatus }, { path: '/tenant/standards/apply-standard', name: 'Apply Standard', component: ApplyStandard }, diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index bb0a0246ca72..568e03211542 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { CAvatar, CDropdown, @@ -7,15 +7,59 @@ import { CDropdownToggle, CLink, } from '@coreui/react' -import { faUser, faBook, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' +import { faUser, faBook, faSignOutAlt, faHistory, faTrash } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Link } from 'react-router-dom' import { authApi } from 'src/store/api/auth' -import { CippProfile, CippOffcanvas } from 'src/components/utilities' +import { CippProfile, CippOffcanvas, CippActionsOffcanvas } from 'src/components/utilities' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' const AppHeaderDropdown = () => { const [profileVisible, setProfileVisible] = useState(false) + const [cippQueueExtendedInfo, setCippQueueExtendedInfo] = useState([]) + const [cippQueueVisible, setCippQueueVisible] = useState(false) + const [cippQueueRefresh, setCippQueueRefresh] = useState('') const { data: profile } = authApi.endpoints.loadClientPrincipal.useQueryState() + + const [getCippQueueList, cippQueueList] = useLazyGenericGetRequestQuery() + + function loadCippQueue() { + setCippQueueVisible(true) + setCippQueueRefresh((Math.random() + 1).toString(36).substring(7)) + getCippQueueList({ path: `api/ListCippQueue?refresh=${cippQueueRefresh}` }) + } + + useEffect(() => { + if (cippQueueList.isFetching) { + setCippQueueExtendedInfo([ + { + label: 'Fetching recent jobs', + value: 'Please wait', + timpestamp: Date(), + link: '#', + }, + ]) + } + if ( + cippQueueList.isSuccess && + Array.isArray(cippQueueList.data) && + cippQueueList.data.length > 0 + ) { + setCippQueueExtendedInfo( + cippQueueList.data?.map((job) => ({ + label: `${job.Name}`, + value: job.Status, + link: job.Link, + timestamp: job.Timestamp, + })), + ) + } else { + setCippQueueExtendedInfo([ + { label: 'No jobs to display', value: '', timpestamp: Date(), link: '#' }, + ]) + } + }, [cippQueueList]) + return ( <> @@ -30,6 +74,10 @@ const AppHeaderDropdown = () => { Profile + loadCippQueue()}> + + Recent Jobs + Logbook @@ -49,6 +97,25 @@ const AppHeaderDropdown = () => { > + , + }, + ]} + placement="end" + visible={cippQueueVisible} + id="cipp-queue" + hideFunction={() => setCippQueueVisible(false)} + /> ) } diff --git a/src/components/header/AppHeaderSearch.js b/src/components/header/AppHeaderSearch.js index d6d33b383420..6b4ab4b50d9a 100644 --- a/src/components/header/AppHeaderSearch.js +++ b/src/components/header/AppHeaderSearch.js @@ -12,8 +12,8 @@ const AppHeaderSearch = () => { }, [dispatch]) return ( <> - - + + ) diff --git a/src/components/layout/AppHeader.js b/src/components/layout/AppHeader.js index 2751e43d4e50..c74f51eec2b1 100644 --- a/src/components/layout/AppHeader.js +++ b/src/components/layout/AppHeader.js @@ -1,9 +1,13 @@ import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { + CAlert, + CAlertLink, CContainer, + CCollapse, CHeader, CHeaderNav, + CNavItem, CHeaderToggler, CImage, CSidebarBrand, @@ -17,16 +21,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCaretSquareLeft, faCaretSquareRight } from '@fortawesome/free-solid-svg-icons' import { toggleSidebarShow } from 'src/store/features/app' import { useMediaPredicate } from 'react-media-hook' +import { useGenericGetRequestQuery } from 'src/store/api/app' const AppHeader = () => { const dispatch = useDispatch() const sidebarShow = useSelector((state) => state.app.sidebarShow) const currentTheme = useSelector((state) => state.app.currentTheme) const preferredTheme = useMediaPredicate('(prefers-color-scheme: dark)') ? 'impact' : 'cyberdrain' + const { + data: dashboard, + isLoading: isLoadingDash, + isSuccess: issuccessDash, + } = useGenericGetRequestQuery({ path: '/api/GetCippAlerts' }) + return ( - - - + <> + + { className="me-2" /> - - - - - + + + + + + + + - - + + + {dashboard && + dashboard.map((item, index) => ( +

+ + {item.Alert} Link + +
+ ))} + ) } diff --git a/src/components/tables/CellBoolean.js b/src/components/tables/CellBoolean.js index 30962b8ec8aa..98f69dfa1adc 100644 --- a/src/components/tables/CellBoolean.js +++ b/src/components/tables/CellBoolean.js @@ -44,13 +44,15 @@ export default function CellBoolean({ cell.toLowerCase() === 'success' || cell.toLowerCase() === 'enabled' || cell.toLowerCase() === 'pass' || - cell.toLowerCase() === 'true' + cell.toLowerCase() === 'true' || + cell.toLowerCase() === 'compliant' ) { normalized = true } else if ( cell.toLowerCase() === 'fail' || cell.toLowerCase() === 'default' || - cell.toLowerCase() === 'false' + cell.toLowerCase() === 'false' || + cell.toLowerCase() === 'noncompliant' ) { normalized = false } diff --git a/src/components/tables/CellDate.js b/src/components/tables/CellDate.js index 91b45eea2f1b..c3934ffcb83d 100644 --- a/src/components/tables/CellDate.js +++ b/src/components/tables/CellDate.js @@ -2,10 +2,14 @@ import React from 'react' import moment from 'moment' import PropTypes from 'prop-types' import { CTooltip } from '@coreui/react' +import TimeAgo from 'javascript-time-ago' +import en from 'javascript-time-ago/locale/en.json' +TimeAgo.addDefaultLocale(en) +import ReactTimeAgo from 'react-time-ago' /** * - * @param format ['short', 'long'] + * @param format ['short', 'long', 'relative'] * @param value * @returns {JSX.Element} * @constructor @@ -27,35 +31,52 @@ export const CellDate = ({ format = 'short', showTime = true, showDate = true, c ] const dateTimeFormatOptions = {} + if (format == 'relative') { + try { + return ( + + + + ) + } catch (error) { + console.error('Error formatting date, fallback to string value', { date: cell, error }) + return ( + +
{String(cell)}
+
+ ) + } + } else { + if (showTime) { + dateTimeFormatOptions.timeStyle = format + } + if (showDate) { + dateTimeFormatOptions.dateStyle = format + } - if (showTime) { - dateTimeFormatOptions.timeStyle = format - } - if (showDate) { - dateTimeFormatOptions.dateStyle = format - } + dateTimeArgs.push(dateTimeFormatOptions) - dateTimeArgs.push(dateTimeFormatOptions) + let formatted - let formatted - try { - // lots of dates returned are unreliably parsable (e.g. non ISO8601 format) - // fallback using moment to parse into date object - formatted = new Intl.DateTimeFormat(...dateTimeArgs).format(moment(cell).toDate()) - } catch (error) { - console.error('Error formatting date, fallback to string value', { date: cell, error }) - formatted = cell - } + try { + // lots of dates returned are unreliably parsable (e.g. non ISO8601 format) + // fallback using moment to parse into date object + formatted = new Intl.DateTimeFormat(...dateTimeArgs).format(moment(cell).toDate()) + } catch (error) { + console.error('Error formatting date, fallback to string value', { date: cell, error }) + formatted = cell + } - return ( - -
{String(formatted)}
-
- ) + return ( + +
{String(formatted)}
+
+ ) + } } CellDate.propTypes = { - format: PropTypes.oneOf(['short', 'medium', 'long', 'full']), + format: PropTypes.oneOf(['short', 'medium', 'long', 'full', 'relative']), cell: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), showTime: PropTypes.bool, showDate: PropTypes.bool, diff --git a/src/components/tables/CellGenericFormat.js b/src/components/tables/CellGenericFormat.js index 4386c3eafbc7..6c31e2d6f4b4 100644 --- a/src/components/tables/CellGenericFormat.js +++ b/src/components/tables/CellGenericFormat.js @@ -79,7 +79,6 @@ export const cellGenericFormatter = return CellTip(cell) } if (Array.isArray(cell) || typeof cell === 'object') { - console.log(cell) return CellTip(JSON.stringify(cell)) } } diff --git a/src/components/tables/CippDatatable.js b/src/components/tables/CippDatatable.js index 1506e93676a8..77b0e5208f7a 100644 --- a/src/components/tables/CippDatatable.js +++ b/src/components/tables/CippDatatable.js @@ -6,11 +6,12 @@ import { CippTablePropTypes } from 'src/components/tables/CippTable' export default function CippDatatable({ path, params, ...rest }) { const [refreshGuid, setRefreshGuid] = React.useState('') + const [graphFilter, setGraphFilter] = React.useState('') const { data = [], isFetching, error, - } = useListDatatableQuery({ path, params: { refreshGuid, ...params } }) + } = useListDatatableQuery({ path, params: { refreshGuid, graphFilter, ...params } }) return ( ) } diff --git a/src/components/tables/CippOffcanvasTable.js b/src/components/tables/CippOffcanvasTable.js index 0dbf984f721f..fe2f7e0e1c7c 100644 --- a/src/components/tables/CippOffcanvasTable.js +++ b/src/components/tables/CippOffcanvasTable.js @@ -22,6 +22,6 @@ CippOffcanvasTable.propTypes = { label: PropTypes.string, value: PropTypes.any, }), - ).isRequired, + ), guid: PropTypes.string, } diff --git a/src/components/tables/CippTable.js b/src/components/tables/CippTable.js index 67f025369f71..0d3f0549d96f 100644 --- a/src/components/tables/CippTable.js +++ b/src/components/tables/CippTable.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useRef } from 'react' import { useSelector } from 'react-redux' import { ExportCsvButton, ExportPDFButton } from 'src/components/buttons' import { @@ -25,7 +25,14 @@ import { ModalService } from '../utilities' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' import { ConfirmModal } from '../utilities/SharedModal' -const FilterComponent = ({ filterText, onFilter, onClear, filterlist, onFilterPreset }) => ( +const FilterComponent = ({ + filterText, + onFilter, + onClear, + filterlist, + onFilterPreset, + onFilterGraph, +}) => ( <> @@ -38,14 +45,29 @@ const FilterComponent = ({ filterText, onFilter, onClear, filterlist, onFilterPr - onFilterPreset('')}>Clear Filter + { + onFilterPreset('') + onFilterGraph('') + }} + > + Clear Filter + {filterlist && filterlist.map((item, idx) => { - return ( - onFilterPreset(item.filter)}> - {item.filterName} - - ) + if (item.hasOwnProperty('graphFilter') && item.graphFilter == true) { + return ( + onFilterGraph(item.filter)}> + {item.filterName} + + ) + } else { + return ( + onFilterPreset(item.filter)}> + {item.filterName} + + ) + } })} @@ -69,6 +91,7 @@ FilterComponent.propTypes = { onClear: PropTypes.func, filterlist: PropTypes.arrayOf(PropTypes.object), onFilterPreset: PropTypes.func, + onFilterGraph: PropTypes.func, } const customSort = (rows, selector, direction) => { @@ -99,6 +122,7 @@ export default function CippTable({ error, reportName, refreshFunction = null, + graphFilterFunction = null, columns = [], dynamicColumns = true, filterlist, @@ -124,6 +148,9 @@ export default function CippTable({ ...rest } = {}, }) { + const inputRef = useRef('') + const [loopRunning, setLoopRunning] = React.useState(false) + const [massResults, setMassResults] = React.useState([]) const [filterText, setFilterText] = React.useState('') const [updatedColumns, setUpdatedColumns] = React.useState(columns) const [selectedRows, setSelectedRows] = React.useState(false) @@ -139,6 +166,16 @@ export default function CippTable({ const filteredItems = data.filter( (item) => JSON.stringify(item).toLowerCase().indexOf(filterText.toLowerCase()) !== -1, ) + const applyFilter = (e) => { + setFilterText(e.target.value) + } + + const setGraphFilter = (e) => { + if (graphFilterFunction) { + graphFilterFunction(e) + } + } + useEffect(() => { if (columns !== updatedColumns) { setUpdatedColumns(columns) @@ -205,11 +242,23 @@ export default function CippTable({ ), title: 'Confirm', - onConfirm: () => - selectedRows.forEach(function (number) { - console.log(number) - genericGetRequest({ path: modalUrl, refreshParam: number }) - }), + onConfirm: async () => { + const resultsarr = [] + for (const row of selectedRows) { + setLoopRunning(true) + const urlParams = new URLSearchParams(modalUrl.split('?')[1]) + for (let [paramName, paramValue] of urlParams.entries()) { + if (paramValue.startsWith('!')) { + urlParams.set(paramName, row[paramValue.replace('!', '')]) + } + } + const NewModalUrl = `${modalUrl.split('?')[0]}?${urlParams.toString()}` + const results = await genericGetRequest({ path: NewModalUrl, refreshParam: row.id }) + resultsarr.push(results) + setMassResults(resultsarr) + } + setLoopRunning(false) + }, }) } else { ModalService.confirm({ @@ -224,17 +273,36 @@ export default function CippTable({ ), title: 'Confirm', - onConfirm: () => [ - genericPostRequest({ - path: modalUrl, - values: { ...modalBody, ...{ input: inputRef.current.value } }, - }), - ], + onConfirm: async () => { + const resultsarr = [] + for (const row of selectedRows) { + setLoopRunning(true) + const urlParams = new URLSearchParams(modalUrl.split('?')[1]) + for (let [paramName, paramValue] of urlParams.entries()) { + if (paramValue.toString().startsWith('!')) { + urlParams.set(paramName, row[paramValue.replace('!', '')]) + } + } + const newModalBody = {} + for (let [objName, objValue] of Object.entries(modalBody)) { + if (objValue.toString().startsWith('!')) { + newModalBody[objName] = row[objValue.replace('!', '')] + } + } + const NewModalUrl = `${modalUrl.split('?')[0]}?${urlParams.toString()}` + const results = await genericPostRequest({ + path: NewModalUrl, + values: { ...modalBody, ...newModalBody, ...{ input: inputRef.current.value } }, + }) + resultsarr.push(results) + setMassResults(resultsarr) + } + setLoopRunning(false) + }, }) } } const executeselectedAction = (item) => { - console.log(item) handleModal(item.modalMessage, item.modalUrl, item.modalType, item.modalBody, item.modalInput) } const defaultActions = [] @@ -365,7 +433,14 @@ export default function CippTable({
setFilterText(e.target.value)} - onFilterPreset={(e) => setFilterText(e)} + onFilterPreset={(e) => { + setFilterText(e) + setGraphFilter('') + }} + onFilterGraph={(e) => { + setFilterText('') + setGraphFilter(e) + }} onClear={handleClear} filterText={filterText} filterlist={filterlist} @@ -393,6 +468,18 @@ export default function CippTable({
{(columns.length === updatedColumns.length || !dynamicColumns) && ( <> + {(massResults.length >= 1 || loopRunning) && ( + + {massResults.map((message, idx) => { + return
  • {message.data.Results}
  • + })} + {loopRunning && ( +
  • + +
  • + )} +
    + )} {selectedRows.length >= 1 && ( - Selected {selectedRows.length} items + Selected {selectedRows.length} items )} )} diff --git a/src/components/utilities/CippActionsOffcanvas.js b/src/components/utilities/CippActionsOffcanvas.js index a3a61d5373fa..b24aa1a28c12 100644 --- a/src/components/utilities/CippActionsOffcanvas.js +++ b/src/components/utilities/CippActionsOffcanvas.js @@ -1,7 +1,12 @@ import React, { useRef } from 'react' import PropTypes from 'prop-types' import { + CButton, CCallout, + CCard, + CCardBody, + CCardText, + CCardTitle, CFormInput, CListGroup, CListGroupItem, @@ -12,8 +17,13 @@ import { CippOffcanvas, ModalService } from 'src/components/utilities' import { CippOffcanvasPropTypes } from 'src/components/utilities/CippOffcanvas' import { CippOffcanvasTable } from 'src/components/tables' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' -import { useNavigate } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { stringCamelCase } from 'src/components/utilities/CippCamelCase' +import TimeAgo from 'javascript-time-ago' + +import en from 'javascript-time-ago/locale/en.json' +TimeAgo.addDefaultLocale(en) +import ReactTimeAgo from 'react-time-ago' export default function CippActionsOffcanvas(props) { const inputRef = useRef('') @@ -91,6 +101,25 @@ export default function CippActionsOffcanvas(props) { var actualUrl = url.replaceAll('{value1}', value1).replaceAll('{value2}', value2) genericGetRequest({ path: actualUrl }) } + let cardContent + try { + cardContent = props.cards.map((action, index) => ( + <> + + + Report Name: {action.label} + + {action.value && Status: {action.value}} + + {action.timestamp && } + + + + )) + } catch (error) { + console.error('An error occurred building OCanvas actions' + error.toString()) + } + const extendedInfoContent = let actionsContent try { @@ -118,7 +147,7 @@ export default function CippActionsOffcanvas(props) { )) } catch (error) { - console.error('An error occored building OCanvas actions' + error.toString()) + console.error('An error occurred building OCanvas actions' + error.toString()) } let actionsSelectorsContent try { @@ -143,7 +172,7 @@ export default function CippActionsOffcanvas(props) { } catch (error) { // When we create an Off Canvas control without selectors we will get this if (!error.toString().includes("Cannot read properties of undefined (reading '")) { - console.error('An error occored building OCanvas selectors' + error.toString()) + console.error('An error occurred building OCanvas selectors' + error.toString()) } } return ( @@ -176,7 +205,9 @@ export default function CippActionsOffcanvas(props) { {getResults.isError && ( Could not connect to API: {getResults.error.message} )} + Extended Information + {cardContent && cardContent} {extendedInfoContent} {Actions} @@ -193,7 +224,7 @@ const CippActionsOffcanvasPropTypes = { label: PropTypes.string, value: PropTypes.any, }), - ).isRequired, + ), actions: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string, diff --git a/src/components/utilities/CippProfile.js b/src/components/utilities/CippProfile.js index 526855ab19e6..e033bc568df9 100644 --- a/src/components/utilities/CippProfile.js +++ b/src/components/utilities/CippProfile.js @@ -12,6 +12,7 @@ import { import { useLoadClientPrincipalQuery } from 'src/store/api/auth' import { ThemeSwitcher, UsageLocation, PageSizeSwitcher } from 'src/components/utilities' import ReportImage from './ReportImage' +import TenantListSelector from './TenantListSelector' const CippProfile = () => { const { data: profile, isFetching, isLoading } = useLoadClientPrincipalQuery() @@ -52,17 +53,18 @@ const CippProfile = () => { -

    - +

    - - - + {!isLoading && } + +

    + + {!isLoading && } ) diff --git a/src/components/utilities/ReportImage.js b/src/components/utilities/ReportImage.js index ed6084dca692..d5aead2b9069 100644 --- a/src/components/utilities/ReportImage.js +++ b/src/components/utilities/ReportImage.js @@ -14,9 +14,7 @@ const ReportImage = () => { const reader = new FileReader() reader.readAsDataURL(e.target.files[0]) reader.onloadend = () => { - console.log(reader.result) dispatch(setReportImage({ reportImage: reader.result })) - console.log(ReportImage) } } diff --git a/src/components/utilities/TenantListSelector.js b/src/components/utilities/TenantListSelector.js new file mode 100644 index 000000000000..94eba4c0bfdc --- /dev/null +++ b/src/components/utilities/TenantListSelector.js @@ -0,0 +1,37 @@ +import React from 'react' +import { CButtonGroup, CButton, CCard, CCardHeader } from '@coreui/react' +import { useDispatch, useSelector } from 'react-redux' +import { setTenantList } from 'src/store/features/app' + +const TenantListSelector = () => { + const dispatch = useDispatch() + const TenantListSelector = useSelector((state) => state.app.TenantListSelector) + + const SwitchPageSize = (value) => { + dispatch(setTenantList({ TenantListSelector: value })) + } + + return ( + + Select default Tenant List + + SwitchPageSize(true)} + active={TenantListSelector ? true : false} + color="secondary" + > + Compressed + + SwitchPageSize(false)} + active={TenantListSelector ? false : true} + color="secondary" + > + Full list + + + + ) +} + +export default TenantListSelector diff --git a/src/components/utilities/TenantSelector.js b/src/components/utilities/TenantSelector.js index 78acb972038e..9a93ea7348eb 100644 --- a/src/components/utilities/TenantSelector.js +++ b/src/components/utilities/TenantSelector.js @@ -7,6 +7,8 @@ import { setCurrentTenant } from 'src/store/features/app' import { CDropdown, CDropdownMenu, CDropdownToggle } from '@coreui/react' import { useNavigate, useSearchParams } from 'react-router-dom' import { queryString } from 'src/helpers' +import { faBuilding } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' const TenantSelector = ({ action, showAllTenantSelector = true, NavSelector = false }) => { const currentTenant = useSelector((state) => state.app.currentTenant) @@ -73,11 +75,17 @@ const TenantSelector = ({ action, showAllTenantSelector = true, NavSelector = fa return ( <> {NavSelector && ( - + + {currentTenant?.defaultDomainName ? ( <> - Selected Tenant: {currentTenant.displayName} + {currentTenant.displayName} ) : ( placeholder diff --git a/src/components/utilities/UniversalSearch.js b/src/components/utilities/UniversalSearch.js index a99bc5a91830..0dadbe464659 100644 --- a/src/components/utilities/UniversalSearch.js +++ b/src/components/utilities/UniversalSearch.js @@ -18,7 +18,7 @@ export const UniversalSearch = React.forwardRef( const handleKeyDown = (event) => { if (event.key === 'Enter') { // on enter key, start the search - getSearchItems({ path: `/api/ExecUniversalSearch?SearchObj=${searchValue}` }) + getSearchItems({ path: `/api/ExecUniversalSearch?name=${searchValue}` }) } } @@ -28,13 +28,13 @@ export const UniversalSearch = React.forwardRef(
    - {searchItems.isSuccess && } + {searchItems.isFetching && ( <>
    @@ -48,6 +48,8 @@ export const UniversalSearch = React.forwardRef(
    )} + {searchItems.isSuccess && } + {searchItems.data <= 1 && 'No results found.'}
    ) }, @@ -78,7 +80,7 @@ const ResultsRow = ({ match }) => { const handleClick = () => { dispatch(hideSwitcher()) - navigate(`/identity/administration/users?customerId=${match.customerId}`) + navigate(`/identity/administration/users?customerId=${match._tenantId}`) } return ( @@ -88,7 +90,7 @@ const ResultsRow = ({ match }) => {
    {match.displayName}
    {match.userPrincipalName}
    - Found in tenant {match.defaultDomainName} + Found in tenant {match._tenantId}
    diff --git a/src/components/utilities/UsageLocation.js b/src/components/utilities/UsageLocation.js index 4dd0d4dddcd3..10ac692681ec 100644 --- a/src/components/utilities/UsageLocation.js +++ b/src/components/utilities/UsageLocation.js @@ -9,7 +9,6 @@ const UsageLocation = () => { const dispatch = useDispatch() const usagelocation = useSelector((state) => state.app.usageLocation) const Switchusage = (t, n) => { - // console.log(t) dispatch(setDefaultusageLocation({ usageLocation: t })) } diff --git a/src/data/standards.json b/src/data/standards.json index 275483c45999..50e671286608 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -57,6 +57,13 @@ "addedComponent": null, "label": "Enable Usernames instead of pseudo anonymised names in reports" }, + { + "name": "standards.DisableGuestDirectory", + "cat": "Global", + "helpText": "This is the default helptext", + "addedComponent": null, + "label": "Restrict guest user access to directory objects" + }, { "name": "standards.ModernAuth", "cat": "Global", @@ -71,6 +78,24 @@ "addedComponent": null, "label": "Disable Basic Authentication" }, + { + "name": "standards.laps", + "cat": "AAD", + "helpText": "Enables the tenant to use LAPS", + "addedComponent": null, + "label": "Enable LAPs on the tenant" + }, + { + "name": "standards.intuneDeviceReg.Enabled", + "cat": "AAD", + "helpText": "This is the default helptext", + "addedComponent": { + "type": "input", + "name": "standards.intuneDeviceReg.max", + "label": "Maximum devices (Enter 2147483647 for unlimited.)" + }, + "label": "Set Maximum Number of Devices per user" + }, { "name": "standards.PWnumberMatchingRequiredState", "cat": "AAD", @@ -150,7 +175,7 @@ "name": "standards.LegacyMFA", "helpText": "This is the default helptext", "addedComponent": null, - "label": "Enable per-user MFA for all user (Legacy)" + "label": "Enable per-user MFA for all user (Legacy, Requires DAP.)" }, { "cat": "AAD", @@ -161,11 +186,18 @@ }, { "cat": "AAD", - "name": "standards.NudgeMFA", + "name": "standards.NudgeMFA.enable", "helpText": "This is the default helptext", "addedComponent": null, "label": "Request to setup Authenticator if not setup yet." }, + { + "cat": "AAD", + "name": "standards.NudgeMFA.disable", + "helpText": "This is the default helptext", + "addedComponent": null, + "label": "Disables the request to setup Authenticator if setup." + }, { "cat": "AAD", "name": "standards.DisableSelfServiceLicenses", @@ -201,6 +233,17 @@ "addedComponent": null, "label": "Enable FIDO2 capabilities" }, + { + "name": "standards.OutBoundSpamAlert.Enabled", + "cat": "Exchange", + "helpText": "This is the default helptext", + "addedComponent": { + "type": "input", + "name": "standards.OutBoundSpamAlert.OutboundSpamContact", + "label": "Outbound spam contact" + }, + "label": "Set Outbound Spam Alert e-mail" + }, { "name": "standards.DisableSharedMailbox", "cat": "Exchange", @@ -230,14 +273,14 @@ "label": "Enable Auto-expanding archives" }, { - "name": "standards.SpoofWarn.Enable", + "name": "standards.SpoofWarn.enable", "cat": "Exchange", "helpText": "This is the default helptext", "addedComponent": null, "label": "Enable Spoofing warnings for Outlook (This e-mail is external identifiers)" }, { - "name": "standards.SpoofWarn.Disable", + "name": "standards.SpoofWarn.disable", "cat": "Exchange", "helpText": "This is the default helptext", "addedComponent": null, @@ -250,6 +293,20 @@ "addedComponent": null, "label": "Disable daily Insight/Viva reports" }, + { + "name": "standards.RotateDKIM", + "cat": "Exchange", + "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", + "addedComponent": null, + "label": "Rotate DKIM keys that are 1024 bit to 2048 bit" + }, + { + "name": "standards.AddDKIM", + "cat": "Exchange", + "helpText": "Enables DKIM for all domains that currently support it", + "addedComponent": null, + "label": "Enables DKIM for all domains that currently support it" + }, { "name": "standards.ActivityBasedTimeout", "cat": "SharePoint", @@ -257,6 +314,48 @@ "addedComponent": null, "label": "Enable 1 hour Activity based Timeout" }, + + { + "name": "standards.calDefault.Enabled", + "cat": "Exchange", + "helpText": "This is the default helptext", + "addedComponent": { + "type": "Select", + "label": "Select Sharing Level", + "name": "standards.calDefault.permissionlevel", + "values": [ + { + "label": "Owner - The user can create, read, edit, and delete all items in the folder, and create subfolders. The user is both folder owner and folder contact.", + "value": "Owner" + }, + { + "label": "Publishing Editor - The user can create, read, edit, and delete all items in the folder, and create subfolders.", + "value": "PublishingEditor" + }, + { + "label": "Reviewer - The user can read all items in the folder.", + "value": "Reviewer" + }, + { + "label": "Editor - The user can create items in the folder. The contents of the folder do not appear.", + "value": "Contributor" + }, + { + "label": "Free Busy Time And Subject And Location - The user can view free/busy time within the calendar and the subject and location of appointments.", + "value": "FreeBusyTimeAndSubjectAndLocation" + }, + { + "label": "Indicates that the user can view only free/busy time within the calendar.", + "value": "FreeBusyTimeOnly" + }, + { + "label": "None - The user has no permissions on the folder.", + "value": "none" + } + ] + }, + "label": "Set Sharing Level for Default calendar" + }, { "name": "standards.sharingCapability.Enabled", "cat": "SharePoint", diff --git a/src/routes.js b/src/routes.js index 6ee996343ab4..b0a689c0a112 100644 --- a/src/routes.js +++ b/src/routes.js @@ -6,6 +6,7 @@ const Users = React.lazy(() => import('src/views/identity/administration/Users') const DeletedItems = React.lazy(() => import('src/views/identity/administration/Deleted')) const ViewBEC = React.lazy(() => import('src/views/identity/administration/ViewBEC')) const AddUser = React.lazy(() => import('src/views/identity/administration/AddUser')) +const InviteGuest = React.lazy(() => import('src/views/identity/administration/InviteGuest')) const EditUser = React.lazy(() => import('src/views/identity/administration/EditUser')) const ViewUser = React.lazy(() => import('src/views/identity/administration/ViewUser')) const Groups = React.lazy(() => import('src/views/identity/administration/Groups')) @@ -16,6 +17,7 @@ const AddGroupTemplates = React.lazy(() => const DeployGroupTemplates = React.lazy(() => import('src/views/identity/administration/DeployGroupTemplate'), ) +const TenantLookup = React.lazy(() => import('src/views/tenant/administration/TenantLookup')) const GroupTemplates = React.lazy(() => import('src/views/identity/administration/GroupTemplates')) const EditGroup = React.lazy(() => import('src/views/identity/administration/EditGroup')) @@ -55,7 +57,7 @@ const DeployConditional = React.lazy(() => import('src/views/tenant/conditional/ const ListLicences = React.lazy(() => import('src/views/tenant/administration/ListLicences')) const ListAppConsent = React.lazy(() => import('src/views/tenant/administration/ListOauthApps')) -const BasicAuthReport = React.lazy(() => import('src/views/identity/reports/BasicAuthReport')) +const InActiveUserReport = React.lazy(() => import('src/views/identity/reports/InactiveUsers')) const SignInReport = React.lazy(() => import('src/views/identity/reports/SignIns')) const AzureADConnectReport = React.lazy(() => @@ -172,6 +174,9 @@ const MailboxClientAccessSettingsList = React.lazy(() => const MailboxStatisticsList = React.lazy(() => import('src/views/email-exchange/reports/MailboxStatisticsList'), ) +const SharedMailboxEnabledAccount = React.lazy(() => + import('src/views/email-exchange/reports/SharedMailboxEnabledAccount'), +) const MessageTrace = React.lazy(() => import('src/views/email-exchange/reports/MessageTrace')) const PhishingPoliciesList = React.lazy(() => import('src/views/email-exchange/reports/PhishingPoliciesList'), @@ -228,6 +233,11 @@ const routes = [ { path: '/identity/administration/users/add', name: 'Add User', component: AddUser }, { path: '/identity/administration/users/edit', name: 'Edit User', component: EditUser }, { path: '/identity/administration/users/view', name: 'View User', component: ViewUser }, + { + path: '/identity/administration/users/InviteGuest', + name: 'Invite Guest', + component: InviteGuest, + }, { path: '/identity/administration/ViewBec', name: 'View BEC', component: ViewBEC }, { path: '/identity/administration', name: 'Administration' }, { path: '/identity/administration/users', name: 'Users', component: Users }, @@ -267,9 +277,9 @@ const routes = [ { path: '/endpoint/reports/devices', name: 'Devices', component: Devices }, { path: '/identity/reports/mfa-report', name: 'MFA Report', component: MFAReport }, { - path: '/identity/reports/basic-auth-report', - name: 'Basic Auth Report', - component: BasicAuthReport, + path: '/identity/reports/inactive-users-report', + name: 'Inactive Users Report', + component: InActiveUserReport, }, { path: '/identity/reports/Signin-report', @@ -359,6 +369,11 @@ const routes = [ name: 'Individual Domain Check', component: IndividualDomain, }, + { + path: '/tenant/administration/tenantlookup', + name: 'Tenant Lookup', + component: TenantLookup, + }, { path: '/tenant/standards/alert-list', name: 'Alert List (Alpha)', component: ListAlerts }, { path: '/endpoint', name: 'Endpoint' }, { path: '/endpoint/applications', name: 'Applications' }, @@ -568,6 +583,11 @@ const routes = [ path: '/email/reports/mailbox-statistics', component: MailboxStatisticsList, }, + { + name: 'Shared Mailbox Enabled Account', + path: '/email/reports/SharedMailboxEnabledAccount', + component: SharedMailboxEnabledAccount, + }, { name: 'Mailbox Client Access Settings', path: '/email/reports/mailbox-cas-settings', diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index 17dd5c4b8ca1..95f65a7aa949 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -530,7 +530,6 @@ } .dropdown-menu.show.tenantDropdown { padding: 1px; - margin-left: 15px; } .header.header-sticky { diff --git a/src/scss/_tenantselector.scss b/src/scss/_tenantselector.scss index 2a8969d4e149..20affa6d0cdc 100644 --- a/src/scss/_tenantselector.scss +++ b/src/scss/_tenantselector.scss @@ -199,6 +199,6 @@ } .tenantDropdown { - min-width: 30rem; + min-width: 25rem; } diff --git a/src/store/features/app.js b/src/store/features/app.js index e4e1124d818d..8fcfea652c71 100644 --- a/src/store/features/app.js +++ b/src/store/features/app.js @@ -10,6 +10,7 @@ const initialState = { currentTheme: 'default', tablePageSize: 25, pageSizes: [25, 50, 100, 200, 500], + TenantListSelector: false, } export const appSlice = createSlice({ @@ -40,6 +41,9 @@ export const appSlice = createSlice({ setReportImage: (state, action) => { state.reportImage = action.payload?.reportImage }, + setTenantList: (state, action) => { + state.TenantListSelector = action.payload?.TenantListSelector + }, }, }) @@ -47,6 +51,7 @@ export const { toggleSidebarShow, toggleSidebarUnfoldable, setCurrentTenant, + setTenantList, setCurrentPageSize, setCurrentTheme, setSidebarVisible, diff --git a/src/views/cipp/AppApproval.js b/src/views/cipp/AppApproval.js index 25febefe28dd..9c957db0707b 100644 --- a/src/views/cipp/AppApproval.js +++ b/src/views/cipp/AppApproval.js @@ -130,20 +130,16 @@ const GraphExplorer = () => { - This tool helps you to retrieve the approval links required for each - tenant. This is required to use 'Application Permissions' within these - tenants when GDAP is deployed. - - This is a temporary measure as Microsoft is adding this as a feature - to GDAP. - -
    -
    - The approval URL might lead to an error page with the error "Admin Role - not found" or not load any page at all after clicking confirm - This is - expected behavior. -
    -
    +

    + This tool helps you to retrieve the approval links required for each + tenant. This is required to use 'Application Permissions' within these + tenants when GDAP is deployed. +

    +

    + The approval URL might lead to an error page with the error "Admin + Role not found" or not load any page at all after clicking confirm - + This is expected behavior. +

    @@ -152,7 +148,7 @@ const GraphExplorer = () => { type="text" name="applicationid" label="Application ID:" - placeholder="Enter the application ID to generate the approval URLs for. This can be any application. Leave blank to generate this for CIPP." + placeholder="Enter the application ID to generate the approval URLs for. This can be any application." /> diff --git a/src/views/cipp/CIPPSettings.js b/src/views/cipp/CIPPSettings.js index dcef6907343f..c29575f64b76 100644 --- a/src/views/cipp/CIPPSettings.js +++ b/src/views/cipp/CIPPSettings.js @@ -21,6 +21,7 @@ import { CSpinner, } from '@coreui/react' import { + useGenericGetRequestQuery, useLazyExecClearCacheQuery, useLazyExecNotificationConfigQuery, useLazyExecPermissionsAccessCheckQuery, @@ -28,6 +29,7 @@ import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery, useLazyListNotificationConfigQuery, + useLoadVersionsQuery, } from 'src/store/api/app' import { useExecAddExcludeTenantMutation, @@ -48,7 +50,13 @@ import { import { useListTenantsQuery } from 'src/store/api/tenants' import { useLazyEditDnsConfigQuery, useLazyGetDnsConfigQuery } from 'src/store/api/domains' import { useDispatch, useSelector } from 'react-redux' -import { cellBooleanFormatter, CellTip, CellTipIcon, CippTable } from 'src/components/tables' +import { + CellBadge, + cellBooleanFormatter, + CellTip, + CellTipIcon, + CippTable, +} from 'src/components/tables' import { CippPage, CippPageList } from 'src/components/layout' import { RFFCFormSwitch, @@ -59,7 +67,12 @@ import { import { Form } from 'react-final-form' import useConfirmModal from 'src/hooks/useConfirmModal' import { setCurrentTenant } from 'src/store/features/app' -import { CippCodeBlock, ModalService, TenantSelectorMultiple } from 'src/components/utilities' +import { + CippCodeBlock, + ModalService, + StatusIcon, + TenantSelectorMultiple, +} from 'src/components/utilities' import CippListOffcanvas from 'src/components/utilities/CippListOffcanvas' import { TitleButton } from 'src/components/buttons' import Skeleton from 'react-loading-skeleton' @@ -129,6 +142,8 @@ const checkAccessColumns = [ ] const GeneralSettings = () => { + const { data: versions, isSuccess: isSuccessVersion } = useLoadVersionsQuery() + const { data: tenants = [] } = useListTenantsQuery({ AllTenantSelector: false }) const [checkPermissions, permissionsResult] = useLazyExecPermissionsAccessCheckQuery() const [clearCache, clearCacheResult] = useLazyExecClearCacheQuery() @@ -271,7 +286,39 @@ const GeneralSettings = () => { return (
    - + + + + Frontend Version + + + +
    Latest: {isSuccessVersion ? versions.RemoteCIPPVersion : }
    +
    Current: {isSuccessVersion ? versions.LocalCIPPVersion : }
    +
    +
    +
    + + + + API Version + + + +
    Latest: {isSuccessVersion ? versions.RemoteCIPPAPIVersion : }
    +
    Current: {isSuccessVersion ? versions.LocalCIPPAPIVersion : }
    +
    +
    +
    +
    + + Permissions Check @@ -497,6 +544,9 @@ const GeneralSettings = () => { + + +
    ) @@ -614,19 +664,6 @@ const ExcludedTenantsSettings = () => { ) } const columns = [ - { - name: 'Latest Status', - selector: (row) => row['GraphErrorCount'], - sortable: true, - cell: (row) => - CellTipIcon( - StatusText(row['GraphErrorCount'], row['LastGraphError']), - StatusIcon(row['GraphErrorCount']), - ), - exportSelector: 'GraphErrorCount', - maxWidth: '130px', - minWidth: '130px', - }, { name: 'Name', selector: (row) => row['displayName'], @@ -641,6 +678,31 @@ const ExcludedTenantsSettings = () => { cell: (row) => CellTip(row['defaultDomainName']), exportSelector: 'defaultDomainName', }, + { + name: 'Relationship Type', + selector: (row) => row['delegatedPrivilegeStatus'], + sortable: true, + cell: (row, index, column) => { + const cell = column.selector(row) + if (!cell) { + return + } + if (cell.toLowerCase() == 'none') { + return + } + if (cell === 'delegatedAdminPrivileges') { + return + } + if (cell === 'delegatedAndGranularDelegetedAdminPrivileges') { + return + } + if (cell === 'granularDelegatedAdminPrivileges') { + return + } + return + }, + exportSelector: 'delegatedPrivilegeStatus', + }, { name: 'Excluded', selector: (row) => row['Excluded'], @@ -702,6 +764,34 @@ const ExcludedTenantsSettings = () => { tenantSelector={false} titleButton={titleButton} datatable={{ + tableProps: { + selectableRows: true, + actionsList: [ + { + label: 'Exclude Tenants', + modal: true, + modalType: 'POST', + modalBody: { + value: '!customerId', + }, + modalUrl: `/api/ExecExcludeTenant?AddExclusion=true`, + modalMessage: 'Are you sure you want to exclude these tenants?', + }, + { + label: 'Include Tenants', + modal: true, + modalUrl: `/api/ExecExcludeTenant?RemoveExclusion=true&TenantFilter=!defaultDomainName`, + modalMessage: 'Are you sure you want to include these tenants?', + }, + { + label: 'Refresh CPV Permissions', + modal: true, + modalUrl: `/api/ExecCPVPermissions?TenantFilter=!customerId`, + modalMessage: + 'Are you sure you want to refresh the CPV permissions for these tenants?', + }, + ], + }, filterlist: [ { filterName: 'Excluded Tenants', filter: '"Excluded":true' }, { filterName: 'Included Tenants', filter: '"Excluded":false' }, @@ -724,11 +814,11 @@ const SecuritySettings = () => { <> - + Resource Group - + The Resource group contains all the CIPP resources in your tenant, except the SAM Application @@ -744,11 +834,11 @@ const SecuritySettings = () => { - + Key Vault - + The keyvault allows you to check token information. By default you do not have access. @@ -764,11 +854,11 @@ const SecuritySettings = () => { - + Static Web App (Role Management) - + The Static Web App role management allows you to invite other users to the application. @@ -786,11 +876,11 @@ const SecuritySettings = () => { - + Function App (Deployment Center) - + The Function App Deployment Center allows you to run updates on the API @@ -805,31 +895,31 @@ const SecuritySettings = () => { - + Function App (Configuration) - + At the Function App Configuration you can check the status of the API access to your keyvault + + Go to Function App Configuration + - - Go to Function App Configuration - - + Function App (Overview) - + At the function App Overview, you can stop and start the backend API @@ -853,7 +943,6 @@ const NotificationsSettings = () => { const [configNotifications, notificationConfigResult] = useLazyExecNotificationConfigQuery() const [listNotification, notificationListResult] = useLazyListNotificationConfigQuery() const onSubmit = (values) => { - console.log(values) configNotifications(values) } return ( @@ -1067,6 +1156,61 @@ const LicenseSettings = () => { ) } +const PasswordSettings = () => { + const [getPasswordConfig, getPasswordConfigResult] = useLazyGenericGetRequestQuery() + const [editPasswordConfig, editPasswordConfigResult] = useLazyGenericPostRequestQuery() + + const [passAlertVisible, setPassAlertVisible] = useState(false) + + const switchResolver = (resolver) => { + editPasswordConfig({ path: '/api/ExecPasswordconfig', values: { passwordType: resolver } }) + getPasswordConfig() + setPassAlertVisible(true) + } + + const resolvers = ['Classic', 'Correct-Battery-Horse'] + + return ( + <> + {getPasswordConfigResult.isUninitialized && + getPasswordConfig({ path: '/api/ExecPasswordConfig?list=true' })} + + + Password Generation + + + Select a password style for generated passwords. + + {resolvers.map((r, index) => ( + switchResolver(r)} + color={ + r === getPasswordConfigResult.data?.Results?.passwordType + ? 'primary' + : 'secondary' + } + key={index} + > + {r} + + ))} + + {(editPasswordConfigResult.isSuccess || editPasswordConfigResult.isError) && ( + + {editPasswordConfigResult.isSuccess + ? editPasswordConfigResult.data.Results + : 'Error setting password style'} + + )} + + + + ) +} + const DNSSettings = () => { const [getDnsConfig, getDnsConfigResult] = useLazyGetDnsConfigQuery() const [editDnsConfig, editDnsConfigResult] = useLazyEditDnsConfigQuery() @@ -1133,7 +1277,6 @@ const Maintenance = () => { } const handleGetLink = () => { - console.log('Making link') listScriptLink({ path: 'api/ExecMaintenanceScripts', params: { ScriptFile: selectedScript, MakeLink: 'True' }, @@ -1197,14 +1340,14 @@ const Maintenance = () => { {listScriptResult.isFetching && ( - + )} {!listScriptResult.isFetching && listScriptResult.isSuccess && ( - + Script Details diff --git a/src/views/cipp/Logs.js b/src/views/cipp/Logs.js index 73a956b18b05..e7733a152fff 100644 --- a/src/views/cipp/Logs.js +++ b/src/views/cipp/Logs.js @@ -20,6 +20,20 @@ import { CippDatatable, cellDateFormatter, CellTip } from 'src/components/tables import { useNavigate } from 'react-router-dom' import DatePicker from 'react-datepicker' import 'react-datepicker/dist/react-datepicker.css' +const reverseSort = (rowA, rowB) => { + const a = rowA.DateTime.toLowerCase() + const b = rowB.DateTime.toLowerCase() + + if (a > b) { + return -1 + } + + if (b > a) { + return 1 + } + + return 0 +} const columns = [ { @@ -30,6 +44,7 @@ const columns = [ exportSelector: 'DateTime', minWidth: '145px', maxWidth: '145px', + sortFunction: reverseSort, }, { name: 'Tenant', @@ -41,11 +56,11 @@ const columns = [ maxWidth: '145px', }, { - name: 'API', - selector: (row) => row['API'], + name: 'User', + selector: (row) => row['User'], sortable: true, - cell: (row) => CellTip(row['API']), - exportSelector: 'API', + cell: (row) => CellTip(row['User']), + exportSelector: 'User', minWidth: '145px', maxWidth: '145px', }, @@ -57,11 +72,11 @@ const columns = [ exportSelector: 'Message', }, { - name: 'User', - selector: (row) => row['User'], + name: 'API', + selector: (row) => row['API'], sortable: true, - cell: (row) => CellTip(row['User']), - exportSelector: 'User', + cell: (row) => CellTip(row['API']), + exportSelector: 'API', minWidth: '145px', maxWidth: '145px', }, diff --git a/src/views/cipp/Setup.js b/src/views/cipp/Setup.js index 5dd85f962c53..0304e311993e 100644 --- a/src/views/cipp/Setup.js +++ b/src/views/cipp/Setup.js @@ -65,14 +65,14 @@ const Setup = () => { const startCIPPSetup = (partner) => { genericGetRequest({ path: 'api/ExecSAMSetup', - params: { CreateSAM: true, partnersetup: partner }, + params: { CreateSAM: true, partnersetup: true }, }) - setNoSubmit(true) + setSetupdone(false) } useInterval( async () => { - if (getResults.data?.step < 7 && getResults.data?.step > 0) { + if (getResults.data?.step < 5 && getResults.data?.step > 0) { genericGetRequest({ path: 'api/ExecSAMSetup', params: { CheckSetupProcess: true, step: getResults.data?.step }, @@ -97,22 +97,26 @@ const Setup = () => {
    Choose Options

    - This wizard will guide you through setting up a SAM application and using the correct keys. - - - + This wizard will guide you through setting up CIPPs access to your client tenants. If this + is your first time setting up CIPP you will want to choose the option "I would like CIPP to + create an application for me". + + + + +
    @@ -124,7 +128,7 @@ const Setup = () => { - Click the buttons below to refresh your tokens. + Click the buttons below to refresh your token.
    Remember to login under a account that has been added to the correct GDAP groups or the group 'AdminAgents'. After confirmation that the refresh is successful, the token cache must be cleared. @@ -147,96 +151,48 @@ const Setup = () => {
    - - - - -

    - When clicking the button below, the setup wizard starts. This is a 5 step process. - Please use a Global Administrator to perform these tasks. You can restart the - process at any time, by clicking on the start button once more. -

    - - startCIPPSetup(true)} - validate={valbutton} - > - Start Setup Wizard - - - -
    -
    - - - {getResults.isFetching && Loading} - {getResults.isSuccess && ( - <> - {getResults.data?.step < 5 ? ( - - ) : ( - - )} - Step {getResults.data?.step} - {getResults.data.message}{' '} - {getResults.data.url && ( - - HERE - - )} - - )} - - -
    - - - - startCIPPSetup(false)} - > - Start Setup Wizard - - - -
    -
    - - - {getResults.isFetching && Loading} - {getResults.isSuccess && ( - <> - {getResults.data?.step < 5 ? ( - - ) : ( - - )} - Step {getResults.data?.step} - {getResults.data.message}{' '} - {getResults.data.url && ( - - HERE - - )} - - )} - - -
    + +

    + When clicking the button below, the setup wizard starts. This is a 5 step process. + Please use a Global Administrator to perform these tasks. You can restart the process + at any time, by clicking on the start button once more. +

    + + startCIPPSetup(true)} + validate={valbutton} + > + Start Setup Wizard + + + + +
    +
    + + + {getResults.isFetching && Loading} + {getResults.isSuccess && ( + <> + {getResults.data?.step < 5 ? ( + + ) : ( + + )} + Step {getResults.data?.step} - {getResults.data.message}{' '} + {getResults.data.url && ( + + HERE + + )} + + )} + +
    you may enter your secrets below, if you only want to update a single value, leave the @@ -281,16 +237,6 @@ const Setup = () => { />
    - - - - -
    diff --git a/src/views/email-exchange/administration/MailboxesList.js b/src/views/email-exchange/administration/MailboxesList.js index 92d3b7d82c91..cc4015b9708e 100644 --- a/src/views/email-exchange/administration/MailboxesList.js +++ b/src/views/email-exchange/administration/MailboxesList.js @@ -17,7 +17,7 @@ const MailboxList = () => { return ( <> @@ -52,7 +52,7 @@ const MailboxList = () => { value: row.UPN, }, { - label: 'Aditional Email Addresses', + label: 'Additional Email Addresses', value: row.AdditionalEmailAddresses ? `${row.AdditionalEmailAddresses}` : 'No additional email addresses', @@ -95,6 +95,20 @@ const MailboxList = () => { modalMessage: 'Are you sure you want to convert this shared mailbox to a user mailbox?', }, + { + label: 'Copy Sent Items to Shared Mailbox', + color: 'info', + modal: true, + modalUrl: `/api/ExecCopyForSent?TenantFilter=${tenant.defaultDomainName}&ID=${row.UPN}`, + modalMessage: 'Are you sure you want to enable Copy Sent Items to Shared Mailbox?', + }, + { + label: 'Disable Copy Sent Items to Shared Mailbox', + color: 'info', + modal: true, + modalUrl: `/api/ExecCopyForSent?TenantFilter=${tenant.defaultDomainName}&ID=${row.UPN}&MessageCopyForSentAsEnabled=false`, + modalMessage: 'Are you sure you want to disable Copy Sent Items to Shared Mailbox?', + }, { label: 'Hide from Global Address List', color: 'info', @@ -111,6 +125,48 @@ const MailboxList = () => { modalMessage: 'Are you sure you want to unhide this mailbox from the global address list? Remember this will not work if the user is AD Synched.', }, + { + label: 'Set Send Quota', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + user: row.UPN, + TenantFilter: tenant.defaultDomainName, + ProhibitSendQuota: true, + }, + modalUrl: `/api/ExecSetMailboxQuota`, + modalInput: true, + modalMessage: 'Enter a quota. e.g. 1000MB, 10GB,1TB', + }, + { + label: 'Set Send and Receive Quota', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + user: row.UPN, + TenantFilter: tenant.defaultDomainName, + ProhibitSendReceiveQuota: true, + }, + modalUrl: `/api/ExecSetMailboxQuota`, + modalInput: true, + modalMessage: 'Enter a quota. e.g. 1000MB, 10GB,1TB', + }, + { + label: 'Set Quota Warning Level', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + user: row.UPN, + TenantFilter: tenant.defaultDomainName, + IssueWarningQuota: true, + }, + modalUrl: `/api/ExecSetMailboxQuota`, + modalInput: true, + modalMessage: 'Enter a quota. e.g. 1000MB, 10GB,1TB', + }, ]} placement="end" visible={ocVisible} @@ -188,6 +244,84 @@ const MailboxList = () => { reportName: `${tenant?.defaultDomainName}-Mailbox-List`, path: '/api/ListMailboxes', columns, + tableProps: { + selectableRows: true, + actionsList: [ + { + label: 'Convert to Shared Mailbox', + color: 'info', + modal: true, + modalUrl: `/api/ExecConvertToSharedMailbox?TenantFilter=${tenant.defaultDomainName}&ID=!UPN`, + modalMessage: 'Are you sure you want to convert this user to a shared mailbox?', + }, + { + label: 'Convert to User Mailbox', + color: 'info', + modal: true, + modalUrl: `/api/ExecConvertToSharedMailbox?TenantFilter=${tenant.defaultDomainName}&ID=!UPN&ConvertToUser=true`, + modalMessage: + 'Are you sure you want to convert this shared mailbox to a user mailbox?', + }, + { + label: 'Hide from Global Address List', + color: 'info', + modal: true, + modalUrl: `/api/ExecHideFromGAL?TenantFilter=${tenant.defaultDomainName}&ID=!UPN&HidefromGAL=true`, + modalMessage: + 'Are you sure you want to hide this mailbox from the global address list? Remember this will not work if the user is AD Synched.', + }, + { + label: 'Unhide from Global Address List', + color: 'info', + modal: true, + modalUrl: `/api/ExecHideFromGAL?TenantFilter=${tenant.defaultDomainName}&ID=!UPN`, + modalMessage: + 'Are you sure you want to unhide this mailbox from the global address list? Remember this will not work if the user is AD Synched.', + }, + { + label: 'Set Send Quota', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + user: '!UPN', + TenantFilter: tenant.defaultDomainName, + ProhibitSendQuota: true, + }, + modalUrl: `/api/ExecSetMailboxQuota`, + modalInput: true, + modalMessage: 'Enter a quota. e.g. 1000MB, 10GB,1TB', + }, + { + label: 'Set Send and Receive Quota', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + user: '!UPN', + TenantFilter: tenant.defaultDomainName, + ProhibitSendReceiveQuota: true, + }, + modalUrl: `/api/ExecSetMailboxQuota`, + modalInput: true, + modalMessage: 'Enter a quota. e.g. 1000MB, 10GB,1TB', + }, + { + label: 'Set Quota Warning Level', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: { + user: '!UPN', + TenantFilter: tenant.defaultDomainName, + IssueWarningQuota: true, + }, + modalUrl: `/api/ExecSetMailboxQuota`, + modalInput: true, + modalMessage: 'Enter a quota. e.g. 1000MB, 10GB,1TB', + }, + ], + }, params: { TenantFilter: tenant?.defaultDomainName }, filterlist: [ { diff --git a/src/views/email-exchange/administration/ViewMobileDevices.js b/src/views/email-exchange/administration/ViewMobileDevices.js index 71e410efbb7e..78c835647e61 100644 --- a/src/views/email-exchange/administration/ViewMobileDevices.js +++ b/src/views/email-exchange/administration/ViewMobileDevices.js @@ -1,89 +1,184 @@ -import React from 'react' +import React, { useState } from 'react' import { useSelector } from 'react-redux' import { CippPageList } from 'src/components/layout' import useQuery from 'src/hooks/useQuery' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { CellTip, cellDateFormatter } from 'src/components/tables' +import { faEye, faEdit, faEllipsisV, faMobileAlt } from '@fortawesome/free-solid-svg-icons' +import { CippActionsOffcanvas } from 'src/components/utilities' +import { Link } from 'react-router-dom' +import { CButton } from '@coreui/react' //TODO: Add CellBoolean -const columns = [ - { - selector: (row) => row['clientType'], - name: 'Client Type', - sortable: true, - cell: (row) => CellTip(row['clientType']), - exportSelector: 'clientType', - }, - { - selector: (row) => row['clientVersion'], - name: 'Client Version', - sortable: true, - exportSelector: 'clientVersion', - }, - { - selector: (row) => row['deviceAccessState'], - name: 'Access State', - sortable: true, - exportSelector: 'deviceAccessState', - }, - { - selector: (row) => row['deviceFriendlyName'], - name: 'Friendly Name', - sortable: true, - cell: (row) => CellTip(row['deviceFriendlyName']), - exportSelector: 'deviceFriendlyName', - }, - { - selector: (row) => row['deviceModel'], - name: 'Model', - sortable: true, - cell: (row) => CellTip(row['deviceModel']), - exportSelector: 'deviceModel', - }, - { - selector: (row) => row['deviceOS'], - name: 'OS', - sortable: true, - cell: (row) => CellTip(row['deviceOS']), - exportSelector: 'deviceOS', - }, - { - selector: (row) => row['deviceType'], - name: 'Device Type', - sortable: true, - cell: (row) => CellTip(row['deviceType']), - exportSelector: 'deviceType', - }, - { - selector: (row) => row['firstSync'], - name: 'First Sync', - sortable: true, - exportSelector: 'firstSync', - cell: cellDateFormatter(), - }, - { - selector: (row) => row['lastSyncAttempt'], - name: 'Last Sync Attempt', - sortable: true, - exportSelector: 'lastSyncAttempt', - cell: cellDateFormatter(), - }, - { - selector: (row) => row['lastSuccessSync'], - name: 'Last Succesfull Sync', - sortable: true, - exportSelector: 'lastSuccessSync', - cell: cellDateFormatter(), - }, - { - selector: (row) => row['status'], - name: 'Status', - sortable: true, - exportSelector: 'status', - }, -] const MobileDeviceList = () => { const tenant = useSelector((state) => state.app.currentTenant) + + const Offcanvas = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + return ( + <> + setOCVisible(true)}> + + + setOCVisible(false)} + /> + + ) + } + + const columns = [ + { + selector: (row) => query.get('userId'), + name: 'User ID', + sortable: true, + cell: (row) => CellTip(query.get('userId')), + exportSelector: 'userId', + }, + { + selector: (row) => row['clientType'], + name: 'Client Type', + sortable: true, + cell: (row) => CellTip(row['clientType']), + exportSelector: 'clientType', + }, + { + selector: (row) => row['clientVersion'], + name: 'Client Version', + sortable: true, + exportSelector: 'clientVersion', + }, + { + selector: (row) => row['deviceAccessState'], + name: 'Access State', + sortable: true, + exportSelector: 'deviceAccessState', + }, + { + selector: (row) => row['deviceFriendlyName'], + name: 'Friendly Name', + sortable: true, + cell: (row) => CellTip(row['deviceFriendlyName']), + exportSelector: 'deviceFriendlyName', + }, + { + selector: (row) => row['deviceModel'], + name: 'Model', + sortable: true, + cell: (row) => CellTip(row['deviceModel']), + exportSelector: 'deviceModel', + }, + { + selector: (row) => row['deviceOS'], + name: 'OS', + sortable: true, + cell: (row) => CellTip(row['deviceOS']), + exportSelector: 'deviceOS', + }, + { + selector: (row) => row['deviceType'], + name: 'Device Type', + sortable: true, + cell: (row) => CellTip(row['deviceType']), + exportSelector: 'deviceType', + }, + { + selector: (row) => row['deviceID'], + name: 'Device ID', + sortable: true, + cell: (row) => CellTip(row['deviceID']), + exportSelector: 'deviceID', + }, + { + selector: (row) => row['firstSync'], + name: 'First Sync', + sortable: true, + exportSelector: 'firstSync', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['lastSyncAttempt'], + name: 'Last Sync Attempt', + sortable: true, + exportSelector: 'lastSyncAttempt', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['lastSuccessSync'], + name: 'Last Succesfull Sync', + sortable: true, + exportSelector: 'lastSuccessSync', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['status'], + name: 'Status', + sortable: true, + exportSelector: 'status', + }, + { + selector: (row) => row['Guid'], + name: 'Guid', + sortable: true, + exportSelector: 'Guid', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '75px', + }, + ] + let query = useQuery() const userId = query.get('userId') return ( @@ -96,9 +191,9 @@ const MobileDeviceList = () => { path: '/api/ListMailboxMobileDevices', columns, params: { TenantFilter: tenant?.defaultDomainName, mailbox: userId }, + selectableRows: true, }} /> ) } - export default MobileDeviceList diff --git a/src/views/email-exchange/reports/MailboxStatisticsList.js b/src/views/email-exchange/reports/MailboxStatisticsList.js index 5a4432e026c2..febb16cc0062 100644 --- a/src/views/email-exchange/reports/MailboxStatisticsList.js +++ b/src/views/email-exchange/reports/MailboxStatisticsList.js @@ -1,80 +1,105 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { CellTip, cellBooleanFormatter } from 'src/components/tables' import { CippPageList } from 'src/components/layout' -const conditionalRowStyles = [ - { - when: (row) => (row.UsedGB / row.QuotaGB) * 100 > 80 && (row.UsedGB / row.QuotaGB) * 100 < 90, - classNames: ['mbusage-warning'], - }, - { - when: (row) => (row.UsedGB / row.QuotaGB) * 100 > 90 && (row.UsedGB / row.QuotaGB) * 100 < 100, - classNames: ['mbusage-danger'], - }, -] - -const columns = [ - { - selector: (row) => row['UPN'], - name: 'User Prinicipal Name', - sortable: true, - cell: (row) => CellTip(row['UPN']), - exportSelector: 'UPN', - minWidth: '200px', - }, - { - selector: (row) => row['displayName'], - name: 'Display Name', - sortable: true, - cell: (row) => CellTip(row['displayName']), - exportSelector: 'displayName', - }, - { - selector: (row) => row['MailboxType'], - name: 'Mailbox Type', - sortable: true, - exportSelector: 'MailboxType', - }, - { - selector: (row) => row['LastActive'], - name: 'Last Active', - sortable: true, - exportSelector: 'LastActive', - }, - { - selector: (row) => row['UsedGB'], - name: 'Used Space(GB)', - sortable: true, - exportSelector: 'UsedGB', - }, - { - selector: (row) => row['QuotaGB'], - name: 'Quota (GB)', - sortable: true, - exportSelector: 'QuotaGB', - }, - { - selector: (row) => row['ItemCount'], - name: 'Item Count (Total)', - sortable: true, - exportSelector: 'ItemCount', - }, - { - selector: (row) => row['HasArchive'], - name: 'Archiving Enabled', - sortable: true, - cell: cellBooleanFormatter({ colourless: true }), - exportSelector: 'HasArchive', - }, -] - const MailboxStatsList = () => { + const [tenantColumnSet, setTenantColumn] = useState(true) const tenant = useSelector((state) => state.app.currentTenant) + const conditionalRowStyles = [ + { + when: (row) => (row.UsedGB / row.QuotaGB) * 100 > 80 && (row.UsedGB / row.QuotaGB) * 100 < 90, + classNames: ['mbusage-warning'], + }, + { + when: (row) => + (row.UsedGB / row.QuotaGB) * 100 > 90 && (row.UsedGB / row.QuotaGB) * 100 < 100, + classNames: ['mbusage-danger'], + }, + ] + 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['UPN'], + name: 'User Prinicipal Name', + sortable: true, + cell: (row) => CellTip(row['UPN']), + exportSelector: 'UPN', + minWidth: '200px', + }, + { + selector: (row) => row['displayName'], + name: 'Display Name', + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + }, + { + selector: (row) => row['MailboxType'], + name: 'Mailbox Type', + sortable: true, + exportSelector: 'MailboxType', + }, + { + selector: (row) => row['LastActive'], + name: 'Last Active', + sortable: true, + exportSelector: 'LastActive', + }, + { + selector: (row) => row['UsedGB'], + name: 'Used Space(GB)', + sortable: true, + exportSelector: 'UsedGB', + }, + { + selector: (row) => row['QuotaGB'], + name: 'Quota (GB)', + sortable: true, + exportSelector: 'QuotaGB', + }, + { + selector: (row) => row['ItemCount'], + name: 'Item Count (Total)', + sortable: true, + exportSelector: 'ItemCount', + }, + { + selector: (row) => row['HasArchive'], + name: 'Archiving Enabled', + sortable: true, + cell: cellBooleanFormatter({ colourless: true }), + exportSelector: 'HasArchive', + }, + ] + useEffect(() => { + if (tenant.defaultDomainName === 'AllTenants') { + setTenantColumn(false) + } + if (tenant.defaultDomainName !== 'AllTenants') { + setTenantColumn(true) + } + }, [tenantColumnSet]) return ( row['UserPrincipalName'], + name: 'User Prinicipal Name', + sortable: true, + cell: (row) => CellTip(row['UserPrincipalName']), + exportSelector: 'UserPrincipalName', + minWidth: '200px', + }, + { + selector: (row) => row['displayName'], + name: 'Display Name', + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + minWidth: '200px', + }, + { + selector: (row) => row['givenName'], + name: 'First Name', + sortable: true, + cell: (row) => CellTip(row['givenName']), + exportSelector: 'givenName', + minWidth: '200px', + }, + { + selector: (row) => row['surname'], + name: 'Surname', + sortable: true, + cell: (row) => CellTip(row['surname']), + exportSelector: 'surname', + minWidth: '200px', + }, + { + selector: (row) => row['accountEnabled'], + name: 'Account Enabled', + sortable: true, + cell: (row) => CellTip(row['accountEnabled']), + exportSelector: 'accountEnabled', + }, +] + +const SharedMailboxEnabledAccount = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + return ( + + ) +} + +export default SharedMailboxEnabledAccount diff --git a/src/views/endpoint/applications/ApplicationsList.js b/src/views/endpoint/applications/ApplicationsList.js index 5d4b1238429f..547f036da607 100644 --- a/src/views/endpoint/applications/ApplicationsList.js +++ b/src/views/endpoint/applications/ApplicationsList.js @@ -134,6 +134,42 @@ const ApplicationsList = () => { reportName: `${tenant?.defaultDomainName}-Applications-List`, path: '/api/ListApps', params: { TenantFilter: tenant?.defaultDomainName }, + tableProps: { + selectableRows: true, + actionsList: [ + { + icon: , + label: ' Assign to All Users', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignApp?AssignTo=AllUsers&TenantFilter=${tenant.defaultDomainName}&ID=!id`, + modalMessage: `Are you sure you want to assign these apps to all users?`, + }, + { + icon: , + label: ' Assign to All Devices', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignApp?AssignTo=AllDevices&TenantFilter=${tenant.defaultDomainName}&ID=!id`, + modalMessage: `Are you sure you want to assign these apps to all devices?`, + }, + { + icon: , + label: ' Assign Globally (All Users / All Devices)', + color: 'info', + modal: true, + modalUrl: `/api/ExecAssignApp?AssignTo=Both&TenantFilter=${tenant.defaultDomainName}&ID=!id`, + modalMessage: `Are you sure you want to assign these apps to all users and devices?`, + }, + { + label: 'Delete Application', + color: 'danger', + modal: true, + modalUrl: `/api/RemoveApp?TenantFilter=${tenant.defaultDomainName}&ID=!id`, + modalMessage: 'Are you sure you want to delete this policy?', + }, + ], + }, }} /> ) diff --git a/src/views/endpoint/intune/Devices.js b/src/views/endpoint/intune/Devices.js index 00ef4cabdb4f..06d28a18c558 100644 --- a/src/views/endpoint/intune/Devices.js +++ b/src/views/endpoint/intune/Devices.js @@ -44,6 +44,22 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { modalUrl: `/api/ExecDeviceAction?TenantFilter=${tenant.defaultDomainName}&GUID=${row.id}&Action=locateDevice`, modalMessage: 'Are you sure you want to locate this device?', }, + { + label: 'Retrieve LAPs password', + color: 'info', + modal: true, + modalUrl: `/api/ExecGetLocalAdminPassword?TenantFilter=${tenant.defaultDomainName}&GUID=${row.azureADDeviceId}`, + modalMessage: 'Are you sure you want to retrieve the local admin password?', + }, + { + label: 'Rotate Local Admin Password', + color: 'info', + modal: true, + modalType: 'POST', + modalBody: {}, + modalUrl: `/api/ExecDeviceAction?TenantFilter=${tenant.defaultDomainName}&GUID=${row.id}&Action=RotateLocalAdminPassword`, + modalMessage: 'Are you sure you want to rotate the password for this device?', + }, { label: 'Retrieve Bitlocker Keys', color: 'info', @@ -84,6 +100,15 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { modalUrl: `/api/ExecDeviceAction?TenantFilter=${tenant.defaultDomainName}&GUID=${row.id}&Action=CreateDeviceLogCollectionRequest`, modalMessage: 'Are you sure you want to generate logs and ship these to MEM?', }, + { + label: 'Rename device', + color: 'info', + modal: true, + modalType: 'POST', + modalInput: true, + modalUrl: `/api/ExecDeviceAction?TenantFilter=${tenant.defaultDomainName}&GUID=${row.id}&Action=setDeviceName`, + modalMessage: 'Enter the new name for the device', + }, { label: 'Fresh Start (Remove user data)', color: 'danger', diff --git a/src/views/home/Home.js b/src/views/home/Home.js index 78fb59f988f4..df7e05d94be5 100644 --- a/src/views/home/Home.js +++ b/src/views/home/Home.js @@ -1,128 +1,292 @@ import React from 'react' -import { faBook, faExclamation, faSearch } from '@fortawesome/free-solid-svg-icons' -import { CButton, CCallout, CCol, CRow } from '@coreui/react' -import { useLoadDashQuery, useLoadVersionsQuery } from 'src/store/api/app' -import { FastSwitcher, StatusIcon } from 'src/components/utilities' +import { + faBook, + faCog, + faEllipsisH, + faEnvelope, + faHotel, + faLaptopCode, + faMailBulk, + faSearch, + faShieldAlt, + faSync, + faUser, + faUserAlt, + faUserFriends, + faUserPlus, + faUsers, + faServer, +} from '@fortawesome/free-solid-svg-icons' +import { CCol, CRow } from '@coreui/react' +import { useGenericGetRequestQuery } from 'src/store/api/app' import { CippContentCard } from 'src/components/layout' -import { CippTable } from 'src/components/tables' -import { Link } from 'react-router-dom' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Skeleton from 'react-loading-skeleton' +import { UniversalSearch } from 'src/components/utilities/UniversalSearch' +import { ActionContentCard } from 'src/components/contentcards' +import { useSelector } from 'react-redux' +import TimeAgo from 'javascript-time-ago' +import allStandardsList from 'src/data/standards' + +import en from 'javascript-time-ago/locale/en.json' +TimeAgo.addDefaultLocale(en) +import ReactTimeAgo from 'react-time-ago' const Home = () => { - const { data: versions, isSuccess: isSuccessVersion } = useLoadVersionsQuery() - const { data: dashboard, isLoading: isLoadingDash, isSuccess: issuccessDash } = useLoadDashQuery() - const tableColumns = [ + const currentTenant = useSelector((state) => state.app.currentTenant) + const { + data: organization, + isLoading: isLoadingOrg, + isSuccess: issuccessOrg, + isFetching: isFetchingOrg, + } = useGenericGetRequestQuery({ + path: '/api/ListOrg', + params: { tenantFilter: currentTenant.defaultDomainName }, + }) + + const { + data: dashboard, + isLoading: isLoadingUserCounts, + isSuccess: issuccessUserCounts, + isFetching: isFetchingUserCount, + } = useGenericGetRequestQuery({ + path: '/api/ListuserCounts', + params: { tenantFilter: currentTenant.defaultDomainName }, + }) + + const { + data: standards, + isLoading: isLoadingStandards, + isSuccess: issuccessStandards, + isFetching: isFetchingStandards, + } = useGenericGetRequestQuery({ + path: '/api/ListStandards', + params: {}, + }) + + const actions1 = [ + { + label: 'M365 Admin', + link: `https://portal.office.com/Partner/BeginClientSession.aspx?CTID=${currentTenant.customerId}&CSDEST=o365admincenter`, + icon: faCog, + }, + { + label: 'Exchange', + link: `https://admin.exchange.microsoft.com/?landingpage=homepage&form=mac_sidebar&delegatedOrg=${currentTenant.defaultDomainName}#`, + icon: faMailBulk, + }, { - name: 'Tenant', - selector: (row) => row['Tenant'], - sortable: true, + label: 'Intune', + link: `https://intune.microsoft.com/${currentTenant.defaultDomainName}`, + icon: faLaptopCode, }, { - name: 'Message', - selector: (row) => row['Message'], - sortable: true, + label: 'Entra', + link: `https://entra.microsoft.com/${currentTenant.defaultDomainName}`, + icon: faUsers, + }, + { + label: 'Security', + link: `https://security.microsoft.com/?tid=${currentTenant.customerId}`, + icon: faShieldAlt, + }, + { + label: 'Azure', + link: `https://portal.azure.com/?tid=${currentTenant.defaultDomainName}`, + icon: faServer, + }, + { + label: 'Sharepoint', + link: `https://admin.microsoft.com/Partner/beginclientsession.aspx?CTID=${currentTenant.customerId}&CSDEST=SharePoint`, + icon: faBook, + }, + ] + + const actions2 = [ + { + label: 'Edit Tenant', + link: `/tenant/administration/tenants/Edit?customerId=${currentTenant.customerId}&tenantFilter=${currentTenant.defaultDomainName}`, + icon: faCog, + }, + { + label: 'List Users', + link: `/identity/administration/users?customerId=${currentTenant.customerId}`, + icon: faUser, + }, + { + label: 'List Groups', + link: `/identity/administration/groups?customerId=${currentTenant.customerId}`, + icon: faUsers, + }, + { + label: 'Create User', + link: `/identity/administration/users/add?customerId=${currentTenant.customerId}`, + icon: faUserPlus, + }, + { + label: 'Create Group', + link: `/identity/administration/groups/add?customerId=${currentTenant.customerId}`, + icon: faUserFriends, }, ] return ( <> - - - + + + - + - - - - {!isLoadingDash && dashboard.Alerts ? ( - dashboard.Alerts.map((mappedAlert, idx) => ( - - {mappedAlert} - - )) - ) : ( - No Active Alerts - )} - - - - -
    {!isLoadingDash ? dashboard?.NextStandardsRun : }
    -
    -
    - - -
    {!isLoadingDash ? dashboard?.NextBPARun : }
    -
    -
    - - -
    {!isLoadingDash ? dashboard?.queuedApps : }
    + + +
    + {issuccessUserCounts && !isFetchingUserCount ? dashboard?.Users : } +
    - - -
    {!isLoadingDash ? dashboard?.queuedStandards : }
    + + +
    + {issuccessUserCounts && !isFetchingUserCount ? dashboard?.LicUsers : } +
    - - -
    {!isLoadingDash ? dashboard?.tenantCount : }
    + + +
    {issuccessUserCounts && !isFetchingUserCount ? dashboard?.Gas : }
    - - -
    - Refresh Token: {!isLoadingDash ? dashboard?.RefreshTokenDate : ''} -
    - -
    - Exchange Token: {!isLoadingDash ? dashboard?.ExchangeTokenDate : ''} + + +
    + {issuccessUserCounts && !isFetchingUserCount ? dashboard?.Guests : }
    - - - -
    Latest: {isSuccessVersion ? versions.RemoteCIPPVersion : }
    -
    Current: {isSuccessVersion ? versions.LocalCIPPVersion : }
    + + + + + + +

    Tenant Name

    + {currentTenant?.displayName} +
    + +

    Tenant ID

    + {currentTenant?.customerId} +
    + +

    Default Domain Name

    + {currentTenant?.defaultDomainName} +
    +
    + + +

    Tenant Status

    + {currentTenant?.delegatedPrivilegeStatus} +
    + +

    Creation Date

    + {(isLoadingOrg || isFetchingOrg) && } + {organization && !isFetchingOrg && organization?.createdDateTime} +
    + +

    AD Connect Status

    + {(isLoadingOrg || isFetchingOrg) && } + {!isLoadingOrg && !isFetchingOrg && organization?.onPremisesSyncEnabled ? ( + <> +
  • + Directory Sync: + {organization?.onPremisesLastSyncDateTime ? ( + + ) : ( + 'Never' + )} +
  • +
  • + Password Sync: + {organization?.onPremisesLastPasswordSyncDateTime ? ( + + ) : ( + 'Never' + )} +
  • + + ) : ( + 'Disabled' + )} +
    +
    + + +

    Domain(s)

    + {(isLoadingOrg || isFetchingOrg) && } + {!isFetchingOrg && + issuccessOrg && + organization?.verifiedDomains?.map((item) =>
  • {item.name}
  • )} +
    + +

    Capabilities

    + {(isLoadingOrg || isFetchingOrg) && } + {!isFetchingOrg && + issuccessOrg && + organization?.assignedPlans + ?.filter((p) => p.capabilityStatus == 'Enabled') + .reduce((plan, curr) => { + if (!plan.includes(curr.service)) { + plan.push(curr.service) + } + return plan + }, []) + .map((plan) => ( + <> + {plan == 'exchange' &&
  • Exchange
  • } + {plan == 'AADPremiumService' &&
  • AAD Premium
  • } + {plan == 'WindowsDefenderATP' &&
  • Windows Defender
  • } + + ))} +
    + +

    Applied Standards

    + {(isLoadingStandards || isFetchingStandards) && } + {issuccessStandards && + !isFetchingStandards && + standards + .filter( + (p) => + p.displayName == 'AllTenants' || + p.displayName == currentTenant.defaultDomainName, + ) + .flatMap((tenant) => { + return Object.keys(tenant.standards).map((standard) => { + const standardDisplayname = allStandardsList.filter((p) => + p.name.includes(standard), + ) + return ( +
  • + {standardDisplayname[0]?.label} ({tenant.displayName}) +
  • + ) + }) + })} +
    +
    - - - -
    Latest: {isSuccessVersion ? versions.RemoteCIPPAPIVersion : }
    -
    Current: {isSuccessVersion ? versions.LocalCIPPAPIVersion : }
    -
    +
    + + + - - - {!isLoadingDash && issuccessDash && ( - - )} - {isLoadingDash && } - - - Jump to log - - - + + diff --git a/src/views/identity/administration/EditGroup.js b/src/views/identity/administration/EditGroup.js index 796992213c1a..bc27feebc675 100644 --- a/src/views/identity/administration/EditGroup.js +++ b/src/views/identity/administration/EditGroup.js @@ -110,6 +110,8 @@ const EditGroup = () => { AddContacts: values.AddContacts ? values.AddContacts : '', RemoveContacts: values.RemoveContacts ? values.RemoveContacts : '', allowExternal: values.allowExternal, + sendCopies: values.sendCopies, + mail: group[0].mail, } //window.alert(JSON.stringify(shippedValues)) genericPostRequest({ path: '/api/EditGroup', values: shippedValues }) @@ -235,6 +237,12 @@ const EditGroup = () => { label="Let people outside the organization email the group" /> )} + {group[0].calculatedGroupType === 'Microsoft 365' && ( + + )} diff --git a/src/views/identity/administration/GroupTemplates.js b/src/views/identity/administration/GroupTemplates.js index 2b64aaee906a..67ce049e327c 100644 --- a/src/views/identity/administration/GroupTemplates.js +++ b/src/views/identity/administration/GroupTemplates.js @@ -119,6 +119,18 @@ const GroupTemplates = () => { path: '/api/ListGroupTemplates', params: { TenantFilter: tenant?.defaultDomainName }, columns, + tableProps: { + selectableRows: true, + actionsList: [ + { + label: 'Delete Template', + color: 'info', + modal: true, + modalUrl: `/api/RemoveGroupTemplate?ID=!GUID`, + modalMessage: 'Are you sure you want to delete these templates?', + }, + ], + }, }} /> diff --git a/src/views/identity/administration/Groups.js b/src/views/identity/administration/Groups.js index de43dac23eec..49400c7206d9 100644 --- a/src/views/identity/administration/Groups.js +++ b/src/views/identity/administration/Groups.js @@ -70,6 +70,13 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { modalMessage: 'Are you sure you want to allow messages from people inside and outside the organisation? Remember this will not work if the group is AD Synched.', }, + { + label: 'Delete Group', + color: 'warning', + modal: true, + modalUrl: `/api/ExecGroupsDelete?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&GroupType=${row.calculatedGroupType}&DisplayName=${row.displayName}`, + modalMessage: 'Are you sure you want to delete this group.', + }, ]} placement="end" visible={ocVisible} @@ -147,6 +154,50 @@ const Groups = () => { reportName: `${tenant?.defaultDomainName}-Groups`, path: '/api/ListGroups', params: { TenantFilter: tenant?.defaultDomainName }, + tableProps: { + selectableRows: true, + actionsList: [ + { + label: 'Hide from Global Address List', + color: 'info', + modal: true, + modalUrl: `/api/ExecGroupsHideFromGAL?TenantFilter=${tenant.defaultDomainName}&ID=!mail&GroupType=!calculatedGroupType&HidefromGAL=true`, + modalMessage: + 'Are you sure you want to hide this mailbox from the global address list? Remember this will not work if the group is AD Synched.', + }, + { + label: 'Unhide from Global Address List', + color: 'info', + modal: true, + modalUrl: `/api/ExecGroupsHideFromGAL?TenantFilter=${tenant.defaultDomainName}&ID=!mail&GroupType=!calculatedGroupType`, + modalMessage: + 'Are you sure you want to unhide this mailbox from the global address list? Remember this will not work if the group is AD Synched.', + }, + { + label: 'Only allow messages from people inside the organisation', + color: 'info', + modal: true, + modalUrl: `/api/ExecGroupsDeliveryManagement?TenantFilter=${tenant.defaultDomainName}&ID=!mail&GroupType=!calculatedGroupType&OnlyAllowInternal=true`, + modalMessage: + 'Are you sure you want to only allow messages from people inside the organisation? Remember this will not work if the group is AD Synched.', + }, + { + label: 'Allow messages from people inside and outside the organisation', + color: 'info', + modal: true, + modalUrl: `/api/ExecGroupsDeliveryManagement?TenantFilter=${tenant.defaultDomainName}&ID=!mail&GroupType=!calculatedGroupType`, + modalMessage: + 'Are you sure you want to allow messages from people inside and outside the organisation? Remember this will not work if the group is AD Synched.', + }, + { + label: 'Delete Group', + color: 'warning', + modal: true, + modalUrl: `/api/ExecGroupsDelete?TenantFilter=${tenant.defaultDomainName}&ID=!mail&GroupType=!calculatedGroupType&DisplayName=!displayName`, + modalMessage: 'Are you sure you want to delete this group.', + }, + ], + }, columns, filterlist: [ { diff --git a/src/views/identity/administration/InviteGuest.js b/src/views/identity/administration/InviteGuest.js new file mode 100644 index 000000000000..c1b46cbdf6fc --- /dev/null +++ b/src/views/identity/administration/InviteGuest.js @@ -0,0 +1,151 @@ +import React from 'react' +import { + CButton, + CCard, + CCardBody, + CCardHeader, + CCardTitle, + CCol, + CForm, + CRow, + CCallout, +} from '@coreui/react' +import { Form } from 'react-final-form' +import { RFFCFormCheck, RFFCFormInput } from 'src/components/forms' +import { CippPage } from 'src/components/layout' +import { useListAdConnectSettingsQuery } from 'src/store/api/adconnect' +import { useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { useSelector } from 'react-redux' +import { required } from 'src/validators' +import useQuery from 'src/hooks/useQuery' +import { useNavigate } from 'react-router-dom' + +const InviteGuest = () => { + let navigate = useNavigate() + + const tenant = useSelector((state) => state.app.currentTenant) + const { defaultDomainName: tenantDomain } = tenant + let query = useQuery() + const allQueryObj = {} + for (const [key, value] of query.entries()) { + allQueryObj[key] = value + } + const { + data: adconnectsettings = [], + isFetching: adcIsFetching, + error: adcError, + } = useListAdConnectSettingsQuery({ tenantDomain }) + + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const onSubmit = (values) => { + const shippedValues = { + DisplayName: values.displayName, + Domain: values.primDomain, + Mail: values.mail, + PostalCode: values.postalCode, + RedirectURL: values.RedirectURL, + SendInvite: values.SendInvite, + tenantID: tenantDomain, + } + //window.alert(JSON.stringify(shippedValues)) + genericPostRequest({ path: '/api/AddGuest', values: shippedValues }) + } + + return ( + + {postResults.isSuccess && ( + + {postResults.data?.Results.map((result, index) => ( +
  • {result}
  • + ))} +
    + )} + + + + + Guest Details + + + {adcError && Unable to determine Azure AD Connect Settings} + {!adcIsFetching && adconnectsettings.dirSyncEnabled && ( + + Warning! {adconnectsettings.dirSyncEnabled} This tenant currently has Active + Directory Sync Enabled. This usually means users should be created in Active + Directory + + )} +
    { + return ( + + + + + + + + + + + + + + + + + + + + + Invite Guest + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + + {postResults.data.Results.map((message, idx) => { + return
  • {message}
  • + })} +
    + )} +
    + ) + }} + /> + + + + + + ) +} + +export default InviteGuest diff --git a/src/views/identity/administration/User365Management.js b/src/views/identity/administration/User365Management.js index fc84d338af80..dbe7711f4892 100644 --- a/src/views/identity/administration/User365Management.js +++ b/src/views/identity/administration/User365Management.js @@ -5,18 +5,18 @@ import PropTypes from 'prop-types' import { ActionContentCard } from 'src/components/contentcards' export default function User365Management({ tenantDomain, userId, className }) { - const azureADLink = `https://portal.azure.com/${tenantDomain}/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/Profile/userId/${userId}` - const endpointManagerLink = `https://endpoint.microsoft.com/${tenantDomain}/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/Profile/userId/${userId}` + const entraLink = `https://entra.microsoft.com/${tenantDomain}/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/${userId}` + const intuneLink = `https://intune.microsoft.com/${tenantDomain}/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/${userId}` const actions = [ { - label: 'View in Azure AD', - link: azureADLink, + label: 'View in Entra', + link: entraLink, icon: faUsers, target: '_blank', }, { - label: 'View in Endpoint Manager', - link: endpointManagerLink, + label: 'View in Intune', + link: intuneLink, icon: faLaptop, target: '_blank', }, diff --git a/src/views/identity/administration/UserActions.js b/src/views/identity/administration/UserActions.js index 4bb0d3818e35..78ca9e360fe8 100644 --- a/src/views/identity/administration/UserActions.js +++ b/src/views/identity/administration/UserActions.js @@ -8,6 +8,7 @@ import { faLockOpen, faUserTimes, faEllipsisH, + faEnvelope, } from '@fortawesome/free-solid-svg-icons' import { ActionContentCard } from 'src/components/contentcards' import { useLazyGenericGetRequestQuery } from 'src/store/api/app' @@ -46,6 +47,16 @@ export default function UserActions({ tenantDomain, userId, userEmail, className `/api/ExecSendPush?TenantFilter=${tenantDomain}&UserEmail=${userEmail}`, ), }, + { + label: 'Enable Online Archive ', + link: '#', + icon: faEnvelope, + onClick: () => + handleModal( + 'Are you sure you want to enable the online archive for this user?', + `/api/ExecEnableArchive?TenantFilter=${tenantDomain}&ID=${userEmail}`, + ), + }, { label: 'Convert to Shared Mailbox', link: '#', diff --git a/src/views/identity/administration/UserDevices.js b/src/views/identity/administration/UserDevices.js index 57a9c1206fbf..b0ad63d8c5be 100644 --- a/src/views/identity/administration/UserDevices.js +++ b/src/views/identity/administration/UserDevices.js @@ -20,7 +20,7 @@ const columns = [ return ( {row.displayName} diff --git a/src/views/identity/administration/UserGroups.js b/src/views/identity/administration/UserGroups.js index 36fbc047b528..fc1a006fa4c8 100644 --- a/src/views/identity/administration/UserGroups.js +++ b/src/views/identity/administration/UserGroups.js @@ -18,7 +18,7 @@ const columns = [ formatter: (cell, row) => { return ( {row.DisplayName} diff --git a/src/views/identity/administration/UserMailboxRuleList.js b/src/views/identity/administration/UserMailboxRuleList.js new file mode 100644 index 000000000000..acb19e3e44e1 --- /dev/null +++ b/src/views/identity/administration/UserMailboxRuleList.js @@ -0,0 +1,98 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { cellBooleanFormatter, CellTip } from 'src/components/tables' +import { DatatableContentCard } from 'src/components/contentcards' +import { faEnvelope } from '@fortawesome/free-solid-svg-icons' + +const rowStyle = (row, rowIndex) => { + const style = {} + + return style +} + +export default function UserMailboxRuleList({ userId, 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', + }, + ] + return ( + + ) +} + +UserMailboxRuleList.propTypes = { + userId: PropTypes.string.isRequired, + tenantDomain: PropTypes.string.isRequired, + className: PropTypes.string, +} diff --git a/src/views/identity/administration/Users.js b/src/views/identity/administration/Users.js index 87d745a824d4..9409f8230b35 100644 --- a/src/views/identity/administration/Users.js +++ b/src/views/identity/administration/Users.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { CButton } from '@coreui/react' import { Link } from 'react-router-dom' import { useSelector } from 'react-redux' @@ -159,14 +159,14 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { label: 'Reset Password (Must Change)', color: 'info', modal: true, - modalUrl: `/api/ExecResetPass?MustChange=true&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}`, + modalUrl: `/api/ExecResetPass?MustChange=true&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&displayName=${row.displayName}`, modalMessage: 'Are you sure you want to reset the password for this user?', }, { label: 'Reset Password', color: 'info', modal: true, - modalUrl: `/api/ExecResetPass?MustChange=false&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}`, + modalUrl: `/api/ExecResetPass?MustChange=false&TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&displayName=${row.displayName}`, modalMessage: 'Are you sure you want to reset the password for this user?', }, { @@ -200,136 +200,233 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { ) } -const columns = [ - { - name: 'Display Name', - selector: (row) => row['displayName'], - sortable: true, - cell: (row) => CellTip(row['displayName']), - exportSelector: 'displayName', - minWidth: '300px', - }, - { - name: 'Email', - selector: (row) => row['mail'], - sortable: true, - cell: (row) => CellTip(row['mail']), - exportSelector: 'mail', - minWidth: '250px', - }, - { - name: 'User Type', - selector: (row) => row['userType'], - sortable: true, - exportSelector: 'userType', - minWidth: '140px', - }, - { - name: 'Enabled', - selector: (row) => row['accountEnabled'], - cell: cellBooleanFormatter({ colourless: true }), - sortable: true, - exportSelector: 'accountEnabled', - minWidth: '100px', - }, - { - name: 'AD Synced', - selector: (row) => row['onPremisesSyncEnabled'], - cell: cellBooleanFormatter({ colourless: true }), - sortable: true, - exportSelector: 'onPremisesSyncEnabled', - minWidth: '120px', - }, - { - name: 'Licenses', - selector: (row) => row['LicJoined'], - exportSelector: 'LicJoined', - sortable: true, - grow: 5, - wrap: true, - minWidth: '200px', - }, - { - name: 'id', - selector: (row) => row['id'], - omit: true, - }, - { - name: 'Actions', - cell: Offcanvas, - }, -] - -const Users = () => { +const Users = (row) => { + const [tenantColumnSet, setTenantColumn] = useState(true) + 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, + }, + { + name: 'Display Name', + selector: (row) => row['displayName'], + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + minWidth: '300px', + }, + { + name: 'Email', + selector: (row) => row['mail'], + sortable: true, + cell: (row) => CellTip(row['mail']), + exportSelector: 'mail', + minWidth: '250px', + }, + { + name: 'User Type', + selector: (row) => row['userType'], + sortable: true, + exportSelector: 'userType', + minWidth: '140px', + }, + { + name: 'Enabled', + selector: (row) => row['accountEnabled'], + cell: cellBooleanFormatter({ colourless: true }), + sortable: true, + exportSelector: 'accountEnabled', + minWidth: '100px', + }, + { + name: 'AD Synced', + selector: (row) => row['onPremisesSyncEnabled'], + cell: cellBooleanFormatter({ colourless: true }), + sortable: true, + exportSelector: 'onPremisesSyncEnabled', + minWidth: '120px', + }, + { + name: 'Licenses', + selector: (row) => row['LicJoined'], + exportSelector: 'LicJoined', + sortable: true, + grow: 5, + wrap: true, + minWidth: '200px', + }, + { + name: 'id', + selector: (row) => row['id'], + omit: true, + }, + { + name: 'Actions', + cell: Offcanvas, + }, + ] const tenant = useSelector((state) => state.app.currentTenant) - const titleButton = + useEffect(() => { + if (tenant.defaultDomainName === 'AllTenants') { + setTenantColumn(false) + } + if (tenant.defaultDomainName !== 'AllTenants') { + setTenantColumn(true) + } + }, [tenantColumnSet]) + + const titleButtons = ( +
    + +
    + +
    +
    + ) return ( { @@ -90,6 +91,9 @@ const ViewUser = (props) => { + + + )} diff --git a/src/views/identity/reports/BasicAuthReport.js b/src/views/identity/reports/BasicAuthReport.js deleted file mode 100644 index d5c56bba3668..000000000000 --- a/src/views/identity/reports/BasicAuthReport.js +++ /dev/null @@ -1,73 +0,0 @@ -import { CLink } from '@coreui/react' -import React from 'react' -import { useSelector } from 'react-redux' -import { CippPageList } from 'src/components/layout' -const columns = [ - { - name: 'User Principal Name', - selector: (row) => row['userPrincipalName'], - sortable: true, - exportSelector: 'userPrincipalName', - }, - { - name: 'Basic Auth', - selector: (row) => row['clientAppUsed'], - sortable: true, - exportSelector: 'clientAppUsed', - }, - { - name: 'Failure Reason', - selector: (row) => row.status?.errorCode, - sortable: true, - exportSelector: 'status', - cell: (row) => { - return ( - - {row.status?.errorCode} - - ) - }, - }, -] - -const Altcolumns = [ - { - name: 'Tenant', - selector: (row) => row['Tenant'], - sortable: true, - exportSelector: 'Tenant', - }, - { - name: 'User Principal Name', - selector: (row) => row['userPrincipalName'], - sortable: true, - exportSelector: 'userPrincipalName', - }, - { - name: 'Basic Auth', - selector: (row) => row['clientAppUsed'], - sortable: true, - exportSelector: 'clientAppUsed', - }, -] -const BasicAuthReport = () => { - const tenant = useSelector((state) => state.app.currentTenant) - - return ( - - ) -} - -export default BasicAuthReport diff --git a/src/views/identity/reports/InactiveUsers.js b/src/views/identity/reports/InactiveUsers.js new file mode 100644 index 000000000000..4c64fc18386e --- /dev/null +++ b/src/views/identity/reports/InactiveUsers.js @@ -0,0 +1,61 @@ +import { CLink } from '@coreui/react' +import React from 'react' +import { useSelector } from 'react-redux' +import { CippPageList } from 'src/components/layout' + +const Altcolumns = [ + { + name: 'Tenant', + selector: (row) => row['tenantDisplayName'], + sortable: true, + exportSelector: 'tenantDisplayName', + }, + { + name: 'User Principal Name', + selector: (row) => row['userPrincipalName'], + sortable: true, + exportSelector: 'userPrincipalName', + }, + { + name: 'Display Name', + selector: (row) => row['displayName'], + sortable: true, + exportSelector: 'displayName', + }, + { + name: 'Last sign in time', + selector: (row) => row['lastNonInteractiveSignInDateTime'], + sortable: true, + exportSelector: 'lastNonInteractiveSignInDateTime', + }, + { + name: 'Assigned Licenses Count', + selector: (row) => row['numberOfAssignedLicenses'], + sortable: true, + exportSelector: 'numberOfAssignedLicenses', + }, + { + name: 'Last updated at', + selector: (row) => row['lastRefreshedDateTime'], + sortable: true, + exportSelector: 'lastRefreshedDateTime', + }, +] +const InActiveUserReport = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + return ( + + ) +} + +export default InActiveUserReport diff --git a/src/views/tenant/administration/EditTenant.js b/src/views/tenant/administration/EditTenant.js index 93d32285f76f..868ed0e77ba2 100644 --- a/src/views/tenant/administration/EditTenant.js +++ b/src/views/tenant/administration/EditTenant.js @@ -40,7 +40,7 @@ const EditTenant = () => { useEffect(() => { if (!tenantDomain || !customerId) { ModalService.open({ - body: 'Error: INvalid request. Could not load requested tenant.', + body: 'Error: Invalid request. Could not load requested tenant.', title: 'Invalid Request', }) setQueryError(true) diff --git a/src/views/tenant/administration/GDAPRoleWizard.js b/src/views/tenant/administration/GDAPRoleWizard.js new file mode 100644 index 000000000000..26c56de51a4d --- /dev/null +++ b/src/views/tenant/administration/GDAPRoleWizard.js @@ -0,0 +1,169 @@ +import React from 'react' +import { CCol, CRow, CForm, CCallout, CSpinner, CButton } 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 { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { RFFCFormInput } from 'src/components/forms' +import { Link } from 'react-router-dom' + +const Error = ({ name }) => ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +const requiredArray = (value) => (value && value.length !== 0 ? undefined : 'Required') + +const GDAPRoleWizard = () => { + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const [genericGetRequest, getResults] = useLazyGenericGetRequestQuery() + + const handleSubmit = async (values) => { + genericPostRequest({ path: '/api/ExecAddGDAPRole', values: values }) + } + + const formValues = {} + + return ( + + +
    +

    Step 1

    +
    + Select which roles you want to map to Groups in your Partner Tenant +
    +
    +
    + + + For each role you select a new group will be created inside of your partner tenant + called "M365 GDAP RoleName". Add your users to these new groups to set their GDAP + permissions. If you need to segment your groups for different teams or to define custom + permissions, use the Custom Suffix to create additional group mappings per role. + + + + + + + + + + {(props) => ( + row['Name'], + sortable: true, + exportselector: 'Name', + }, + { + name: 'Description', + selector: (row) => row['Description'], + sortable: true, + }, + ]} + fieldProps={props} + /> + )} + + + + + +
    +
    + +
    +

    Step 2

    +
    Confirm and apply
    +
    +
    + {!postResults.isSuccess && ( + + {(props) => { + return ( + <> + + + +
    Roles and group names
    + + {props.values.gdapRoles.map((role, idx) => ( +
  • + {role.Name} - M365 GDAP {role.Name} +
  • + ))} +
    + {props.values.customSuffix != null && ( + <> +
    Custom Group Suffix
    + +
  • {props.values.customSuffix}
  • +
    + + )} +
    +
    + + ) + }} +
    + )} + {postResults.isFetching && ( + + Loading + + )} + {postResults.isSuccess && ( + <> + + {postResults.data.Results.map((message, idx) => { + return
  • {message}
  • + })} +
    + + Start GDAP Migration + + + )} +
    +
    +
    + ) +} + +export default GDAPRoleWizard diff --git a/src/views/tenant/administration/GDAPWizard.js b/src/views/tenant/administration/GDAPWizard.js index 6ccac1b57599..b8c110be6c9c 100644 --- a/src/views/tenant/administration/GDAPWizard.js +++ b/src/views/tenant/administration/GDAPWizard.js @@ -5,6 +5,7 @@ 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 { TitleButton } from 'src/components/buttons' import PropTypes from 'prop-types' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' @@ -45,7 +46,7 @@ const GDAPWizard = () => { onSubmit={handleSubmit} wizardTitle="GDAP Migration Wizard" > - +

    Step 1

    Setup GDAP Migration tool
    @@ -82,48 +83,13 @@ const GDAPWizard = () => { )}
    - -
    -

    Step 2

    -
    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 3

    +

    Step 2

    Select which roles you want to add to GDAP relationship.
    @@ -131,31 +97,30 @@ const GDAPWizard = () => {
    - For each role you select a new group will be created inside of your partner tenant - called "M365 GDAP RoleName". Add your users to these new groups to set their GDAP - permissions. -

    CIPP will create a single relationship with all roles you've selected for the maximum duration of 730 days using a GUID as a random name for the relationship.
    It is recommend to put CIPP user in the correct GDAP Role Groups to manage your environment secure after deployment of GDAP.
    +
    + +
    {(props) => ( row['Name'], + selector: (row) => row['RoleName'], sortable: true, exportselector: 'Name', }, { - name: 'Description', - selector: (row) => row['Description'], + name: 'Group', + selector: (row) => row['GroupName'], sortable: true, }, ]} @@ -167,6 +132,42 @@ const GDAPWizard = () => {

    + +
    +

    Step 3

    +
    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 4

    @@ -193,7 +194,7 @@ const GDAPWizard = () => { {props.values.gdapRoles.map((role, idx) => (
  • - {role.Name} - M365 GDAP {role.Name} + {role.RoleName} - {role.GroupName}
  • ))}
    diff --git a/src/views/tenant/administration/ListAlertsQueue.js b/src/views/tenant/administration/ListAlertsQueue.js index c963d2ffd149..485c11b5444c 100644 --- a/src/views/tenant/administration/ListAlertsQueue.js +++ b/src/views/tenant/administration/ListAlertsQueue.js @@ -163,6 +163,18 @@ const ListAlertsQueue = () => { reportName: `AlertsQueue-List`, path: '/api/ListAlertsQueue', params: { TenantFilter: tenant?.defaultDomainName }, + tableProps: { + selectableRows: true, + actionsList: [ + { + label: 'Delete alerts', + color: 'info', + modal: true, + modalUrl: `/api/RemoveQueuedAlert?ID=!tenantId`, + modalMessage: 'Are you sure you want to delete these alerts?', + }, + ], + }, }} />
    diff --git a/src/views/tenant/administration/ListGDAPRelationships.js b/src/views/tenant/administration/ListGDAPRelationships.js new file mode 100644 index 000000000000..f44b5da4d144 --- /dev/null +++ b/src/views/tenant/administration/ListGDAPRelationships.js @@ -0,0 +1,123 @@ +import { CButton } from '@coreui/react' +import { faEllipsisV, faTrashAlt } 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 { cellDateFormatter, cellNullTextFormatter } from 'src/components/tables' +import { CippActionsOffcanvas } from 'src/components/utilities' + +const Actions = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + + const tenant = useSelector((state) => state.app.currentTenant) + return ( + <> + setOCVisible(true)}> + + + , + modalUrl: `/api/ExecDeleteGDAPRelationship?GDAPID=${row.id}`, + modalMessage: 'Are you sure you want to delete this relationship?', + }, + ]} + placement="end" + visible={ocVisible} + id={row.id} + hideFunction={() => setOCVisible(false)} + /> + + ) +} + +const GDAPRelationships = () => { + const columns = [ + { + name: 'Tenant', + selector: (row) => row.customer?.displayName, + sortable: true, + exportSelector: 'customer', + cell: cellNullTextFormatter(), + }, + { + name: 'Relationship Name', + selector: (row) => row['displayName'], + sortable: true, + exportSelector: 'displayName', + }, + { + name: 'Status', + selector: (row) => row['status'], + sortable: true, + exportSelector: 'status', + }, + { + name: 'Created', + selector: (row) => row['createdDateTime'], + sortable: true, + exportSelector: 'createdDateTime', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'Activated', + selector: (row) => row['activatedDateTime'], + sortable: true, + exportSelector: 'activatedDateTime', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'End', + selector: (row) => row['endDateTime'], + sortable: true, + exportSelector: 'endDateTime', + cell: cellDateFormatter({ format: 'short' }), + }, + { + name: 'Actions', + cell: Actions, + maxWidth: '80px', + }, + ] + return ( +
    + +
    + ) +} + +export default GDAPRelationships diff --git a/src/views/tenant/administration/ListGDAPRoles.js b/src/views/tenant/administration/ListGDAPRoles.js new file mode 100644 index 000000000000..f7279740b20c --- /dev/null +++ b/src/views/tenant/administration/ListGDAPRoles.js @@ -0,0 +1,42 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { CSpinner, CCallout } from '@coreui/react' +import { CippPageList } from 'src/components/layout' +import { TitleButton } from 'src/components/buttons' + +const ListGDAPRoles = () => { + const columns = [ + { + name: 'Role', + selector: (row) => row['RoleName'], + sortable: true, + exportSelector: 'RoleName', + }, + { + name: 'Group', + selector: (row) => row['GroupName'], + sortable: true, + exportSelector: 'GroupName', + }, + ] + return ( +
    + + } + tenantSelector={false} + datatable={{ + keyField: 'id', + columns, + reportName: `GDAPRole-List`, + path: '/api/ListGDAPRoles', + }} + /> +
    + ) +} + +export default ListGDAPRoles diff --git a/src/views/tenant/administration/ServiceHealth.js b/src/views/tenant/administration/ServiceHealth.js index 492bbf7ebbdb..30d3a5e0199c 100644 --- a/src/views/tenant/administration/ServiceHealth.js +++ b/src/views/tenant/administration/ServiceHealth.js @@ -1,5 +1,6 @@ import React from 'react' import { CippPageList } from 'src/components/layout' +import { CellTip } from 'src/components/tables' const columns = [ { @@ -30,6 +31,7 @@ const columns = [ name: 'Description', selector: (row) => row['desc'], sortable: true, + cell: (row) => CellTip(row['desc']), exportSelector: 'desc', }, ] diff --git a/src/views/tenant/administration/TenantLookup.js b/src/views/tenant/administration/TenantLookup.js new file mode 100644 index 000000000000..a9faad2d0cc4 --- /dev/null +++ b/src/views/tenant/administration/TenantLookup.js @@ -0,0 +1,156 @@ +import React, { useEffect, useState } from 'react' +import { + CButton, + CCard, + CCardBody, + CCardHeader, + CCardTitle, + CCol, + CCollapse, + CForm, + CFormInput, + CInputGroup, + CRow, + CSpinner, +} from '@coreui/react' +import useQuery from 'src/hooks/useQuery' +import { Field, Form } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBook, faSearch } from '@fortawesome/free-solid-svg-icons' +import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' +import { CippContentCard } from 'src/components/layout' +import Skeleton from 'react-loading-skeleton' +import { domainsApi } from 'src/store/api/domains' + +const GraphExplorer = () => { + let navigate = useNavigate() + const tenant = useSelector((state) => state.app.currentTenant) + let query = useQuery() + const tenantdomain = query.get('tenant') + const SearchNow = query.get('SearchNow') + const [visibleA, setVisibleA] = useState(true) + const handleSubmit = async (values) => { + setVisibleA(false) + + const shippedValues = { + tenant: values.domain, + SearchNow: true, + 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() + + useEffect(() => { + if (tenantdomain) { + execGraphRequest({ + path: 'api/ListExternalTenantInfo', + params: { + tenant: tenantdomain, + }, + }) + } + }, [execGraphRequest, tenant.defaultDomainName, query, tenantdomain]) + const isValidDomain = (value) => + /^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$/i.test( + value, + ) + ? undefined + : value + + return ( + + + + + + + Domain + + + + { + return ( + + + {({ input, meta }) => { + return ( + <> + + + + Check{graphrequest.isFetching && } + + + + ) + }} + + + ) + }} + /> + + + + {tenantdomain && ( + + + + +

    Tenant Name

    + {graphrequest.isFetching && } + {graphrequest.data?.GraphRequest.displayName} +
    + +

    Tenant ID

    + {graphrequest.isFetching && } + {graphrequest.data?.GraphRequest.tenantId} +
    + +

    Default Domain Name

    + {graphrequest.isFetching && } + {graphrequest.data?.GraphRequest.defaultDomainName} +
    +
    + + +

    Tenant Brand Name

    + {graphrequest.isFetching && } + {graphrequest.data?.GraphRequest.federationBrandName} + {graphrequest.data?.GraphRequest.federationBrandName === null && + 'No brand name set'} +
    + +

    Domains

    + {graphrequest.isFetching && } + {graphrequest.data?.Domains && + graphrequest.data?.Domains.map((domainname) =>
  • {domainname}
  • )} +
    +
    +
    +
    + )} +
    + ) +} + +export default GraphExplorer diff --git a/src/views/tenant/administration/Tenants.js b/src/views/tenant/administration/Tenants.js index 98f74a9ba4f6..a181ae12a798 100644 --- a/src/views/tenant/administration/Tenants.js +++ b/src/views/tenant/administration/Tenants.js @@ -100,10 +100,10 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { }, { icon: , - label: 'AAD Portal', + label: 'Entra Portal', color: 'info', external: true, - link: `https://aad.portal.azure.com/${row.defaultDomainName}`, + link: `https://entra.microsoft.com/${row.defaultDomainName}`, }, { icon: , @@ -124,7 +124,7 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { label: 'MEM (Intune) Portal', color: 'info', external: true, - link: `https://endpoint.microsoft.com/${row.defaultDomainName}`, + link: `https://intune.microsoft.com/${row.defaultDomainName}`, }, { icon: , @@ -167,8 +167,9 @@ function StatusText(graphErrorCount, lastGraphError) { } const TenantsList = () => { + const TenantListSelector = useSelector((state) => state.app.TenantListSelector) const tenant = useSelector((state) => state.app.currentTenant) - const [columnOmits, setOmitVisible] = useState(true) + const [columnOmits, setOmitVisible] = useState(TenantListSelector) const columns = [ { diff --git a/src/views/tenant/conditional/ConditionalAccess.js b/src/views/tenant/conditional/ConditionalAccess.js index ce4b49a60989..205816829601 100644 --- a/src/views/tenant/conditional/ConditionalAccess.js +++ b/src/views/tenant/conditional/ConditionalAccess.js @@ -75,7 +75,7 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { modal: true, icon: , modalUrl: `/api/RemoveCAPolicy?TenantFilter=${tenant.defaultDomainName}&GUID=${row.id}`, - modalMessage: 'Are you sure you want to disable this rule?', + modalMessage: 'Are you sure you want to delete this rule?', }, ]} placement="end" @@ -223,6 +223,43 @@ const ConditionalAccessList = () => { path: '/api/ListConditionalAccessPolicies', params: { TenantFilter: tenant?.defaultDomainName }, columns, + tableProps: { + selectableRows: true, + actionsList: [ + { + label: 'Enable Rule', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditCAPolicy?State=Enabled&TenantFilter=${tenant.defaultDomainName}&GUID=!id`, + modalMessage: 'Are you sure you want to enable this rule?', + }, + { + label: 'Disable Rule', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditCAPolicy?State=Disabled&TenantFilter=${tenant.defaultDomainName}&GUID=!id`, + modalMessage: 'Are you sure you want to disable this rule?', + }, + { + label: 'Set rule to report only', + color: 'info', + icon: , + modal: true, + modalUrl: `/api/EditCAPolicy?State=enabledForReportingButNotEnforced&TenantFilter=${tenant.defaultDomainName}&GUID=!id`, + modalMessage: 'Are you sure you want to disable this rule?', + }, + { + label: 'Delete Rule', + color: 'danger', + modal: true, + icon: , + modalUrl: `/api/RemoveCAPolicy?TenantFilter=${tenant.defaultDomainName}&GUID=!id`, + modalMessage: 'Are you sure you want to delete this rule?', + }, + ], + }, }} /> ) diff --git a/src/views/tenant/standards/ApplyStandard.js b/src/views/tenant/standards/ApplyStandard.js index b62a8aea06d4..86a3f08786e4 100644 --- a/src/views/tenant/standards/ApplyStandard.js +++ b/src/views/tenant/standards/ApplyStandard.js @@ -28,7 +28,12 @@ const Error = ({ name }) => ( Error.propTypes = { name: PropTypes.string.isRequired, } - +function getDeepKeys(obj) { + return Object.keys(obj) + .filter((key) => obj[key] instanceof Object) + .map((key) => getDeepKeys(obj[key]).map((k) => `${key}.${k}`)) + .reduce((x, y) => x.concat(y), Object.keys(obj)) +} const ApplyStandard = () => { const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() @@ -296,11 +301,22 @@ const ApplyStandard = () => {
    Selected Standards
    - {Object.entries(props.values.standards).map(([key, value]) => - allStandardsList - .filter((obj) => obj.name.includes(key)) - .map((item, idx) =>
  • {item.label}
  • ), - )} + {getDeepKeys(props.values.standards) + .reduce((acc, key) => { + const existingItem = allStandardsList.find((obj) => + obj.name.includes(key), + ) + if ( + existingItem && + !acc.find((item) => item.name === existingItem.name) + ) { + acc.push(existingItem) + } + return acc + }, []) + .map((item, idx) => ( +
  • {item.label}
  • + ))}

    diff --git a/version_latest.txt b/version_latest.txt index a0cd9f0ccb01..3c8ff8c36b50 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -3.1.0 \ No newline at end of file +3.5.1 \ No newline at end of file