diff --git a/.github/ISSUE_TEMPLATE/CLA.yml b/.github/ISSUE_TEMPLATE/CLA.yml deleted file mode 100644 index c2180bb38d05..000000000000 --- a/.github/ISSUE_TEMPLATE/CLA.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: CLA - Contributor License Agreement -description: Sign the CLA for CIPP and CIPP-API -labels: [CLA] - -body: -- type: markdown - attributes: - value: > - CONTRIBUTOR LICENSE AGREEMENT ("Agreement") - - Version 1.0 - - 1. Definitions - - "Contribution" means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Kelvin Tegelaar for inclusion in, or documentation of, any of the products owned or managed by Kelvin Tegelaar (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Kelvin Tegelaar or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Kelvin Tegelaar for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - - "You" (or "Your") means the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Kelvin Tegelaar. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single contributor. This Agreement applies both to future Contributions and Contributions made prior to the date of this Agreement. - - 2. Grant of Copyright License - - Subject to the terms and conditions of this Agreement, You hereby grant to Kelvin Tegelaar and to recipients of software distributed by Kelvin Tegelaar a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works for the purpose of dual licensing the application. - - 3. Grant of Patent License - - You grant Kelvin Tegelaar, and those who receive the Contribution directly or indirectly from Kelvin Tegelaar, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under Your patent claims that are necessarily infringed by the Contribution or the combination of the Contribution with the Project to which it was submitted, to make, have made, use, offer to sell, sell, import, and otherwise dispose of the Contribution alone or with the Project. - - 4. Other Rights Reserved - - Each party reserves all rights not expressly granted in this Agreement. No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are granted by implication, exhaustion, estoppel, or otherwise. - - You are not expected to provide support for your Contributions, except to the extent you desire to provide - support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in - writing, you provide your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF - ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES - OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A - PARTICULAR PURPOSE. - - 5. Representations - - You represent that you are legally entitled to grant the above licenses. If your employer(s) has rights to intellectual property that you create, you represent that you have received permission to make Contributions on behalf of that employer, or that your employer has waived such rights for your Contributions to Kelvin Tegelaar. - - You represent that each of Your Contributions is Your original creation. You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. - - 6. Project Sale - - In the event that the Project is sold or otherwise transferred in ownership in its entirety to a third party, a sum equivalent to fifteen percent (15%) of the total sale price or value of the consideration received shall be set aside. This sum shall be divided amongst all Contributors who have entered into this Agreement, with each Contributor receiving a portion proportional to the relative quantity and significance of their Contributions to the Project, as determined by Kelvin Tegelaar. The method and timeframe of the distribution shall be at the discretion of Kelvin Tegelaar and shall be communicated - -- type: textarea - attributes: - label: Description - description: > - Type "I Agree" in the text area. - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 1e0e6afd8faa..db613f4551c2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,42 +1,92 @@ -name: 🐞 Bug report -description: Report errors or unexpected behaviors for CIPP and CIPP-API -labels: [unconfirmed-by-user , bug] +name: "🐞 Bug report" +description: "Report errors or unexpected behaviors for CIPP and CIPP-API" +title: "[Bug]: " +labels: + - "unconfirmed-by-user" + - "bug" body: -- type: markdown - attributes: - value: > - Thanks for reporting. - - - Make sure you are able to reproduce this issue on the latest released version of CIPP & CIPP-API. - - - Please search the existing issues to see if there has been a similar issue filed - - - This is not the location for support. Issues that request support or are not a bug will be closed. -- type: textarea - attributes: - label: Description - description: > - Please describe the issue and expected result. You can include a screenshot by pasting it. Issues with a description that is too short or does not explain each step in detail will be closed. - - Example: - - 1.) go to Settings - 2.) Click on a tenant in access check - 3.) Click on the green pixel in the bottom right corner - 4.) A bug appears. - validations: - required: true -- type: textarea - attributes: - label: Environment data - description: > - 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 + - type: markdown + attributes: + value: | + **Thank you for taking the time to report a potential bug for CIPP and CIPP-API!** + + Please follow the instructions below and provide as much detail as possible to help us understand and reproduce the issue. + + - type: checkboxes + id: confirmations + attributes: + label: "Required confirmations before submitting" + description: "Please check all boxes that apply." + options: + - label: "**I can reproduce this issue on the latest released versions** of both CIPP and CIPP-API." + required: true + - label: "**I have searched existing issues** (both open and closed) to avoid duplicates." + required: true + - label: "I am **not** requesting general support; this is an actual bug report." + required: true + + # 3) Description / Steps to reproduce + - type: textarea + id: description + attributes: + label: "Issue Description" + description: | + **Describe the issue clearly and provide step-by-step instructions to reproduce it.** + Screenshots can be attached by pasting them here. + + Example steps to reproduce: + 1. Go to **Settings** + 2. Click on a tenant in **Access Check** + 3. Click on the green pixel in the bottom-right corner + 4. Observe the unexpected behavior + validations: + required: true + + # 4) Environment type (Sponsored vs. Non-sponsored) + - type: dropdown + id: environment_type + attributes: + label: "Environment Type" + description: "Select whether you are using currently a paying user of the product, or if you are utilizing the free version" + options: + - "Sponsored (paying) user" + - "Non-sponsored user" + validations: + required: true + + # 5) Front End / Back End versions + - type: input + id: front_end_version + attributes: + label: "Front End Version" + description: "Please specify the front end version number (e.g., v1.2.3)." + validations: + required: true + + - type: input + id: back_end_version + attributes: + label: "Back End Version" + description: "Please specify the back end version number (e.g., v1.2.3)." + validations: + required: true + + # 7) Additional logs or trace (optional) + - type: textarea + id: logs + attributes: + label: "Relevant Logs / Stack Trace" + description: | + If available, please share any relevant logs or stack trace data. + Remove or redact any sensitive info before posting. + render: plaintext + validations: + required: false + + # 8) Closing note + - type: markdown + attributes: + value: | + Thank you for your submission! A maintainer will review your report. + Please watch the issue for follow-up questions or status updates. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..9fa384883c3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false +contact_links: + - name: Security Reports + url: https://github.com/KelvinTegelaar/CIPP/security/advisories + about: Please report security vulnerabilities here. + - name: Community Discord + url: https://discord.gg/cyberdrain + about: Join our discord community here. + diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index de839daf7a30..4978afebd026 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,32 +1,84 @@ -name: ✨ Feature request -description: Suggest a new feature or improvement -title: '[Feature Request]: ' -labels: [enhancement, no-priority] +name: "✨ Feature request" +description: "Suggest a new feature or improvement" +title: "[Feature Request]: " +labels: + - "enhancement" + - "no-priority" body: -- type: markdown - attributes: - value: > - Thanks for suggesting a feature! - - - Please search the existing feature request to see if there has been a similar issue filed. - - - If a feature has been filed before, but not followed up by a contributor, you can develop the feature yourself by checking the development documentation [here](https://docs.cipp.app/dev-documentation/cipp-dev-guide/setting-up-for-local-development). - - - Repeat feature requests are allowed if the previous request has been closed for more than 30 days - - - drive-by feature requests without effort will be closed. - - - A feature request has 14 days to be fullfilled before automatically being closed. if you want to work on the feature yourself use the phrase "I'd like to work on this please!" - - - Feature requests that are detrimental to security will also be closed without notice. -- type: textarea - attributes: - label: Description of the new feature - must be an in-depth explanation of the feature you want, reasoning why, and the added benefits for MSPs as a whole. - validations: - required: true -- type: textarea - attributes: - label: PowerShell commands you would normally use to achieve above request - validations: - required: false + # Introductory Markdown + - type: markdown + attributes: + value: | + **Thank you for suggesting a new feature or improvement for CIPP** + + Before creating a request, please: + + 1. Check that you have an active sponsorship, only users that are sponsoring CIPP at the $99,- sponsorship level can create feature requests. + 1. Search existing **open and closed** feature requests to avoid duplicates. + 2. Note that **repeat feature requests** are permitted if a previous request was closed more than 30 days ago. + 3. Consider implementing the feature yourself by reviewing the [development documentation](https://docs.cipp.app/dev-documentation/cipp-dev-guide/setting-up-for-local-development). + 4. Feature requests that lack sufficient detail or feasibility may be closed at any time. + 5. **This request will auto-close in 14 days** if no meaningful progress or collaboration occurs. + 6. If you would like to work on this feature, comment `"I'd like to work on this please!"` + 7. Any request that is detrimental to security or the product’s stability will be closed without notice. + + # Checkboxes for Confirmations + - type: checkboxes + id: confirmations + attributes: + label: "Please confirm:" + description: "Check all boxes that apply." + options: + - label: "**I have searched existing feature requests** (open and closed) and found no duplicates." + required: true + - label: "**me or my organization is currently an active sponsor of the product at the $99,- level." + required: true + + - type: textarea + id: problem-statement + attributes: + label: "Problem Statement" + description: | + **What problem does this feature solve or what gap does it fill?** + Provide a concise explanation. For example: + - "When I need to enable MFA for all users, it takes a lot of time to do it per user. I'd like to enable this for all users in button press" + - "To update a user property for exchange I need to go through 5 portals and 2 systems to be able to make a change." + validations: + required: true + + # Field 2: Benefits for MSPs + - type: textarea + id: msp-benefits + attributes: + label: "Benefits for MSPs" + description: | + **How would this feature help MSPs in their day-to-day tasks or overall operations?** + - Does it reduce manual work? + - Does it improve security or scalability? + - Does it offer clarity or automation to commonly repeated tasks? + validations: + required: true + + # Field 3: Value or Importance + - type: textarea + id: feature-value + attributes: + label: "Value or Importance" + description: | + **Why is this feature particularly valuable or important to add?** + - If it's critical, explain why. + - If it's optional or nice-to-have, describe how it still adds notable value. + validations: + required: true + + # Optional field for PowerShell commands + - type: textarea + id: powershell-commands + attributes: + label: "PowerShell Commands (Optional)" + description: | + If you currently achieve this functionality or a similar workaround using PowerShell, please share your scripts or snippets here. + This information helps contributors understand the existing workflow and aids in development. + validations: + required: false diff --git a/.github/workflows/Check_for_Version_Update.yml b/.github/workflows/Check_for_Version_Update.yml index ec6f3e68769b..f8a7db0343f2 100644 --- a/.github/workflows/Check_for_Version_Update.yml +++ b/.github/workflows/Check_for_Version_Update.yml @@ -7,17 +7,17 @@ on: jobs: build: if: github.repository_owner == 'KelvinTegelaar' - name: 'Check for Version Update' + name: "Check for Version Update" runs-on: ubuntu-latest steps: - name: Check for Changed Files uses: brettcannon/check-for-changed-files@v1.1.0 with: - file-pattern: version_latest.txt - failure-message: 'You have not updated version_latest.txt. This is a required file to update at each PR. Please sync your latest changes and update the version number.' + file-pattern: public/version.json + failure-message: "You have not updated version.json. This is a required file to update at each PR. Please sync your latest changes and update the version number." - name: Prevent changes to workflow files uses: DovnarAlexander/github-action-file-detection@v0.3.0 with: - wildcard: '.github/workflows/*.yml' + wildcard: ".github/workflows/*.yml" exit_code_found: 1 exit_code_not_found: 0 diff --git a/.github/workflows/Comment_on_Issues.yml b/.github/workflows/Comment_on_Issues.yml index 38e9a5405a28..8d83f6237d82 100644 --- a/.github/workflows/Comment_on_Issues.yml +++ b/.github/workflows/Comment_on_Issues.yml @@ -16,15 +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://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!". - add-comment_fr: - if: github.repository_owner == 'KelvinTegelaar' && github.event.label.name == 'enhancement' - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Add Comment - uses: peter-evans/create-or-update@v3 + Thank you for reporting a potential bug. If you would like to work on this bug, please comment: + > I would like to work on this please! + + Thank you for helping us maintain the project! diff --git a/.github/workflows/auto_comments.yml b/.github/workflows/auto_comments.yml new file mode 100644 index 000000000000..7d7b11b45474 --- /dev/null +++ b/.github/workflows/auto_comments.yml @@ -0,0 +1,83 @@ +name: "Handle Comment Commands" + +on: + issue_comment: + types: + - created + +jobs: + handle_comment: + runs-on: ubuntu-latest + # We need permissions to modify issue comments. + # 'issues: write' is required for deleting comments. + permissions: + issues: write + + steps: + # 1) If the comment includes '!notasponsor', delete it using GitHub Script + - name: Delete !notasponsor comment + if: contains(github.event.comment.body, '!notasponsor') + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id + }); + + # 2) Post a sponsor-specific reply + - name: Reply to !notasponsor + if: contains(github.event.comment.body, '!notasponsor') + uses: peter-evans/create-or-update-comment@v3 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Hello, + + Thank you for your interest in improving CIPP! + To keep our development process focused and manageable, **feature requests are limited to paying users**. This policy helps us prioritize improvements that directly benefit those actively supporting CIPP and ensures we can sustain our development and support. + + When a sponsor makes a feature request, their support covers training, development, documentation, and security checks. Allowing non-sponsor requests could lead to a backlog that slows down updates and stretches resources thin, ultimately affecting the quality and sustainability of CIPP. + + While we’ve closed this request, we appreciate your input. You’re always welcome to participate in ongoing discussions or contribute to open issues. If you are a developer, feel free to open a PR that includes your feature request or comment "**I’d like to work on this!**" to assign the issue to yourself. + + **Did you get this notification in error?** Reply with a screenshot of your sponsorship payment and we’ll reopen the issue. + + _Thank you for understanding,_ + **The CIPP Team** + + # 3) If the comment includes '!support', classify as a support request + - name: Reply to !support + if: contains(github.event.comment.body, '!support') + uses: peter-evans/create-or-update-comment@v3 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Hello, + + Thank you for reaching out! This report has been classified as a **support request** rather than a bug or feature request. To keep our development process focused, support requests are limited to paying users. This policy allows us to prioritize resources for those actively supporting CIPP, helping us maintain high-quality development and support. + + Sponsors can contact our helpdesk directly via email for assistance with any issues or questions. For non-sponsor support, please refer to our documentation and community discussions—many questions have been answered there. + + **Did you get this notification in error?** Reply with a screenshot of your sponsorship payment, and we’ll gladly reopen the request. + + _Thank you for your understanding,_ + **The CIPP Team** + + # 4) If the comment includes '!incomplete', note the bug or feature request is incomplete + - name: Reply to !incomplete + if: contains(github.event.comment.body, '!incomplete') + uses: peter-evans/create-or-update-comment@v3 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Hello, + + Thank you for your submission! It appears this **bug report or feature request is incomplete**. We need a clear description, steps to reproduce (for bugs), or a comprehensive overview of the requested feature. + + Please submit a new request with all the necessary details. Without sufficient information, it’s difficult for contributors to triage or implement solutions. + + _Thank you!_ + **The CIPP Team** diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml new file mode 100644 index 000000000000..834f1995b668 --- /dev/null +++ b/.github/workflows/cipp_dev_build.yml @@ -0,0 +1,49 @@ +name: CIPP Frontend Dev Build + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + if: github.event.repository.fork == false + name: Build and Upload CIPP Frontend + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout Code + uses: actions/checkout@v4.2.2 + + # Set up Node.js + - name: Set up Node.js + uses: actions/setup-node@v4.2.0 + with: + node-version: '20.18.1' + + # Install dependencies + - name: Install Dependencies + run: yarn install + + # Build the project + - name: Build Project + run: npm run build + + # Create ZIP File in a New Source Directory + - name: Prepare and Zip Build Files + run: | + mkdir -p build + cp staticwebapp.config.json out/ + zip -r build/dev.zip out + + # Upload to Azure Blob Storage + - name: Azure Blob Upload + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.3.1 + with: + connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} + container_name: cipp + source_folder: build/ + destination_folder: / + delete_if_exists: true diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml new file mode 100644 index 000000000000..3d6d1c79550e --- /dev/null +++ b/.github/workflows/cipp_frontend_build.yml @@ -0,0 +1,49 @@ +name: CIPP Frontend Build + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + if: github.event.repository.fork == false + name: Build and Upload CIPP Frontend + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout Code + uses: actions/checkout@v4.2.2 + + # Set up Node.js + - name: Set up Node.js + uses: actions/setup-node@v4.2.0 + with: + node-version: '20.18.1' + + # Install dependencies + - name: Install Dependencies + run: yarn install + + # Build the project + - name: Build Project + run: npm run build + + # Create ZIP File in a New Source Directory + - name: Prepare and Zip Build Files + run: | + mkdir -p build + cp staticwebapp.config.json out/ + zip -r build/latest.zip out + + # Upload to Azure Blob Storage + - name: Azure Blob Upload + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.3.1 + with: + connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} + container_name: cipp + source_folder: build/ + destination_folder: / + delete_if_exists: true diff --git a/Tools/Start-CippDevEmulators.ps1 b/Tools/Start-CippDevEmulators.ps1 index 882579f1bdc4..d171c0a1eaaf 100644 --- a/Tools/Start-CippDevEmulators.ps1 +++ b/Tools/Start-CippDevEmulators.ps1 @@ -1,8 +1,14 @@ -Write-Host 'Starting CIPP Dev Emulators' +Get-Command wt -ErrorAction Stop | Out-Null Get-Process node -ErrorAction SilentlyContinue | Stop-Process -ErrorAction SilentlyContinue $Path = (Get-Item $PSScriptRoot).Parent.Parent.FullName -$Process = Read-Host -Prompt 'Start Process Function (y/N)?' +pwsh -file (Join-Path $PSScriptRoot 'Start-CippDevInstallation.ps1') + +Write-Host 'Starting CIPP Dev Emulators' + +if (Test-Path (Join-Path $Path 'CIPP-API-Processor')) { + $Process = Read-Host -Prompt 'Start Process Function (y/N)?' +} if ($Process -eq 'y') { wt --title CIPP`; new-tab --title 'Azurite' -d $Path pwsh -c azurite`; new-tab --title 'FunctionApp' -d $Path\CIPP-API pwsh -c func start`; new-tab --title 'CIPP Frontend' -d $Path\CIPP pwsh -c npm run dev`; new-tab --title 'SWA' -d $Path\CIPP pwsh -c npm run start-swa`; new-tab --title 'CIPP-API-Processor' -d $Path\CIPP-API-Processor pwsh -c func start --port 7072 diff --git a/Tools/Start-CippDevEmulatorsWithKitty.ps1 b/Tools/Start-CippDevEmulatorsWithKitty.ps1 index 39666bdcb05c..7013c054f375 100644 --- a/Tools/Start-CippDevEmulatorsWithKitty.ps1 +++ b/Tools/Start-CippDevEmulatorsWithKitty.ps1 @@ -15,7 +15,7 @@ if ($Process -eq 'y') { kitty @new-window --new-tab --tab-title `"Azurite`" --cwd $Path -- azurite ; kitty @new-window --new-tab --tab-title `"FunctionApp`" --cwd (Join-Path $Path `"CIPP-API`") -- func start; kitty @new-window --new-tab --tab-title `"CIPP Frontend`" --cwd (Join-Path $Path `"CIPP`") -- yarn run dev ; - kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- npm run start-swa; + kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- yarn run start-swa; kitty @new-window --new-tab --tab-title `"CIPP-API-Processor`" --cwd (Join-Path $Path `"CIPP-API-Processor`") -- func start --port 7072" } else { @@ -23,5 +23,5 @@ if ($Process -eq 'y') { kitty @new-window --new-tab --tab-title `"Azurite`" --cwd $Path -- azurite ; kitty @new-window --new-tab --tab-title `"FunctionApp`" --cwd (Join-Path $Path `"CIPP-API`") -- func start; kitty @new-window --new-tab --tab-title `"CIPP Frontend`" --cwd (Join-Path $Path `"CIPP`") -- yarn run dev ; - kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- npm run start-swa" + kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- yarn run start-swa" } diff --git a/Tools/Start-CippDevInstallation.ps1 b/Tools/Start-CippDevInstallation.ps1 index fa1de465b2eb..2d00a12c6a26 100644 --- a/Tools/Start-CippDevInstallation.ps1 +++ b/Tools/Start-CippDevInstallation.ps1 @@ -1,32 +1,32 @@ $Path = (Get-Item $PSScriptRoot).Parent.Parent.FullName -if (-not(Get-Command npm)) { - throw 'npm is required to install the CIPP development environment' +if (-not((Get-Command npm -ErrorAction SilentlyContinue) -or (Get-Command yarn -ErrorAction SilentlyContinue))) { + throw 'npm or yarn is required to install the CIPP development environment.' } -if (-not(Get-Command azurite)) { +if (-not(Get-Command yarn -ErrorAction SilentlyContinue)) { + Write-Host 'Installing Yarn' + npm install --global yarn +} + +if (-not(Get-Command azurite -ErrorAction SilentlyContinue)) { Write-Host 'Installing Azurite' - npm install --global 'azurite' + yarn global add 'azurite' } -if (-not(Get-Command swa)) { +if (-not(Get-Command swa -ErrorAction SilentlyContinue)) { Write-Host 'Installing @azure/static-web-apps-cli' - npm install --global '@azure/static-web-apps-cli' + yarn global add '@azure/static-web-apps-cli' } -if (-not(Get-Command func)) { +if (-not(Get-Command func -ErrorAction SilentlyContinue)) { Write-Host 'Installing Azure Functions Core Tools' - npm install --global 'azure-functions-core-tools@4' --unsafe-perms true -} - -if (-not(Get-Command yarn)) { - Write-Host 'Installing Yarn' - npm install --global yarn + yarn global add 'azure-functions-core-tools@4' } -if (-not(yarn list --global --pattern 'next' | Select-String -Pattern 'next')) { +if (-not(yarn global list | Select-String -Pattern 'next')) { Write-Host 'Installing Next.js' - yarn install --global next --network-timeout 500000 + yarn global add 'next' } yarn install --cwd (Join-Path $Path "CIPP") --network-timeout 500000 diff --git a/deployment/AzureDeploymentTemplate.json b/deployment/AzureDeploymentTemplate.json index 91ce52c678a7..54cacf9f1064 100644 --- a/deployment/AzureDeploymentTemplate.json +++ b/deployment/AzureDeploymentTemplate.json @@ -241,6 +241,19 @@ "functionAppResourceId": "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", "functionAppRegion": "[resourceGroup().location]" } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, variables('funcAppName'), 'Contributor')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('funcAppName'))]" + ], + "properties": { + "roleDefinitionId": "[concat(subscription().id, '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('funcAppName')),'2019-08-01', 'full').identity.principalId]", + "scope": "[resourceGroup().id]" + } } ], "outputs": {} diff --git a/deployment/AzureDeploymentTemplate_regionoptions.json b/deployment/AzureDeploymentTemplate_regionoptions.json index 7e23726404c2..71e0cef97aca 100644 --- a/deployment/AzureDeploymentTemplate_regionoptions.json +++ b/deployment/AzureDeploymentTemplate_regionoptions.json @@ -241,6 +241,19 @@ "functionAppResourceId": "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", "functionAppRegion": "[resourceGroup().location]" } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, variables('funcAppName'), 'Contributor')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('funcAppName'))]" + ], + "properties": { + "roleDefinitionId": "[concat(subscription().id, '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('funcAppName')),'2019-08-01', 'full').identity.principalId]", + "scope": "[resourceGroup().id]" + } } ], "outputs": {} diff --git a/generate-placeholders.js b/generate-placeholders.js index 9858fc7f7b7c..2f6b614fe9a8 100644 --- a/generate-placeholders.js +++ b/generate-placeholders.js @@ -35,11 +35,11 @@ const pages = [ { title: "Backup Wizard", path: "/tenant/backup/backup-wizard" }, { title: "Restore Wizard", path: "/tenant/backup/restore-wizard" }, { title: "Tools", path: "/tenant/administration" }, - { title: "Graph Explorer", path: "/tenant/administration/graph-explorer" }, - { title: "Application Approval", path: "/tenant/administration/appapproval" }, + { title: "Graph Explorer", path: "/tenant/tools/graph-explorer" }, + { title: "Application Approval", path: "/tenant/tools/appapproval" }, { title: "IP Database", path: "/tenant/tools/geoiplookup" }, { title: "Tenant Lookup", path: "/tenant/administration/tenantlookup" }, - { title: "Individual Domain Check", path: "/tenant/standards/individual-domains" }, + { title: "Individual Domain Check", path: "/tenant/tools/individual-domains" }, { title: "BPA Report Builder", path: "/tenant/tools/bpa-report-builder" }, { title: "Standards", path: "/tenant/standards" }, { title: "Edit Standards", path: "/tenant/standards/list-applied-standards" }, @@ -82,7 +82,7 @@ const pages = [ { title: "Add Profile", path: "/endpoint/autopilot/add-profile" }, { title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" }, { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" }, - { title: "Devices", path: "/endpoint/reports/devices" }, + { title: "Devices", path: "/endpoint/MEM/devices" }, { title: "Configuration Policies", path: "/endpoint/MEM/list-policies" }, { title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" }, { title: "Protection Policies", path: "/endpoint/MEM/list-appprotection-policies" }, @@ -108,9 +108,9 @@ const pages = [ { title: "Transport rules", path: "/email/transport/list-rules" }, { title: "Deploy Transport rule", path: "/email/transport/deploy-rules" }, { title: "Transport Templates", path: "/email/transport/list-templates" }, - { title: "Connectors", path: "/email/connectors/list-connectors" }, + { title: "Connectors", path: "/email/transport/list-connectors" }, { title: "Deploy Connector Templates", path: "/email/connectors/deploy-connector" }, - { title: "Connector Templates", path: "/email/connectors/list-connector-templates" }, + { title: "Connector Templates", path: "/email/transport/list-connector-templates" }, { title: "Spamfilter", path: "/email/spamfilter/list-spamfilter" }, { title: "Apply Spamfilter Template", path: "/email/spamfilter/deploy" }, { title: "Templates", path: "/email/spamfilter/list-templates" }, diff --git a/package.json b/package.json index 6c2c57ba925d..13dac459bf3f 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "cipp", - "version": "7.0.0", + "version": "7.1.3", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { "url": "https://github.com/KelvinTegelaar/CIPP/issues" }, + "license": "AGPL-3.0", "engines": { - "node": "^18.17.0" + "node": "^20.18.1" }, "repository": { "type": "git", diff --git a/public/assets/integrations/github.png b/public/assets/integrations/github.png new file mode 100644 index 000000000000..e03d8dd8bcf0 Binary files /dev/null and b/public/assets/integrations/github.png differ diff --git a/public/assets/integrations/github_dark.png b/public/assets/integrations/github_dark.png new file mode 100644 index 000000000000..c61ab9d0582e Binary files /dev/null and b/public/assets/integrations/github_dark.png differ diff --git a/public/discord-mark-blue.svg b/public/discord-mark-blue.svg new file mode 100644 index 000000000000..4cadbc7f7ed3 --- /dev/null +++ b/public/discord-mark-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/version.json b/public/version.json index fa68337146a1..542517aee22f 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "7.1.1" + "version": "7.2.3" } diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index c9c62c1f8048..fa953247204c 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -190,7 +190,11 @@ export function ApiGetCallWithPagination({ return response.data; }, getNextPageParam: (lastPage) => { - if (data?.noPagination || data?.manualPagination === false) { + if ( + data?.noPagination || + data?.manualPagination === false || + data?.tenantFilter === "AllTenants" + ) { return undefined; } return lastPage?.Metadata?.nextLink ? { nextLink: lastPage.Metadata.nextLink } : undefined; diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index aa7fc42a8bca..32e8aee05e8d 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -143,7 +143,7 @@ export const CippBannerListCard = (props) => { {isCollapsible && ( - + {item?.propertyItems?.length > 0 && ( diff --git a/src/components/CippCards/CippButtonCard.jsx b/src/components/CippCards/CippButtonCard.jsx index d02a21b7a697..73f1009e1ec7 100644 --- a/src/components/CippCards/CippButtonCard.jsx +++ b/src/components/CippCards/CippButtonCard.jsx @@ -23,6 +23,7 @@ export default function CippButtonCard({ variant, component = "card", accordionExpanded = false, + onAccordionChange, }) { const [cardExpanded, setCardExpanded] = useState(accordionExpanded); useEffect(() => { @@ -31,6 +32,12 @@ export default function CippButtonCard({ } }, [accordionExpanded]); + useEffect(() => { + if (onAccordionChange) { + onAccordionChange(cardExpanded); + } + }, [cardExpanded]); + return ( {component === "card" && ( diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx index 0da5fb61de97..286e7cc35808 100644 --- a/src/components/CippCards/CippDomainCards.jsx +++ b/src/components/CippCards/CippDomainCards.jsx @@ -510,7 +510,7 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) - + { )} - {secondHalf.map((item, index) => ( + {isFetching ? ( } /> - ))} + ) : ( + secondHalf.map((item, index) => ( + + )) + )} )} @@ -142,22 +152,18 @@ export const CippPropertyListCard = (props) => { key={`${item.label}-${index}-ActionList-OffCanvas`} icon={{item.icon}} label={item.label} - onClick={ - item.link - ? () => window.open(item.link, "_blank") - : () => { - setActionData({ - data: data, - action: item, - ready: true, - }); - if (item?.noConfirm) { - item.customFunction(item, data, {}); - } else { - createDialog.handleOpen(); - } - } - } + onClick={() => { + setActionData({ + data: data, + action: item, + ready: true, + }); + if (item?.noConfirm) { + item.customFunction(item, data, {}); + } else { + createDialog.handleOpen(); + } + }} disabled={handleActionDisabled(data, item)} /> ))} diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx index 8b9048f46fec..49c0ec716ab1 100644 --- a/src/components/CippCards/CippUserInfoCard.jsx +++ b/src/components/CippCards/CippUserInfoCard.jsx @@ -91,6 +91,11 @@ export const CippUserInfoCard = (props) => { ) } /> + : user?.id || "N/A"} + /> { row = {}, relatedQueryKeys, dialogAfterEffect, + allowResubmit = false, ...other } = props; const router = useRouter(); const [addedFieldData, setAddedFieldData] = useState({}); const [partialResults, setPartialResults] = useState([]); + const [isFormSubmitted, setIsFormSubmitted] = useState(false); + + useEffect(() => { + if (createDialog.open) { + setIsFormSubmitted(false); + formHook.reset(); + } + }, [createDialog.open]); + const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, @@ -79,7 +89,6 @@ export const CippApiDialog = (props) => { } else { Object.keys(dataObject).forEach((key) => { const value = dataObject[key]; - if (typeof value === "string" && value.startsWith("!")) { newData[key] = value.slice(1); } else if (typeof value === "string") { @@ -104,6 +113,7 @@ export const CippApiDialog = (props) => { }; const tenantFilter = useSettings().currentTenant; const handleActionClick = (row, action, formData) => { + setIsFormSubmitted(true); if (action.multiPost === undefined) { action.multiPost = false; } @@ -208,14 +218,63 @@ export const CippApiDialog = (props) => { const onSubmit = (data) => handleActionClick(row, api, data); const selectedType = api.type === "POST" ? actionPostRequest : actionGetRequest; + useEffect(() => { + if (api?.setDefaultValues && createDialog.open) { + fields.map((field) => { + if ( + ((typeof row[field.name] === "string" && field.type === "textField") || + (typeof row[field.name] === "boolean" && field.type === "switch")) && + row[field.name] !== undefined && + row[field.name] !== null && + row[field.name] !== "" + ) { + formHook.setValue(field.name, row[field.name]); + } else if (Array.isArray(row[field.name]) && field.type === "autoComplete") { + var values = []; + row[field.name].map((element) => { + if (element.label && element.value) { + values.push(element); + } else if (typeof element === "string" || typeof element === "number") { + values.push({ + label: element, + value: element, + }); + } + }); + formHook.setValue(field.name, values); + } else if ( + field.type === "autoComplete" && + row[field.name] !== "" && + (typeof row[field.name] === "string" || + (typeof row[field.name] === "object" && + row[field.name] !== undefined && + row[field.name] !== null)) + ) { + if (typeof row[field.name] === "string") { + formHook.setValue(field.name, { + label: row[field.name], + value: row[field.name], + }); + } else if ( + typeof row[field.name] === "object" && + row[field.name]?.label && + row[field.name]?.value + ) { + formHook.setValue(field.name, row[field.name]); + } + } + }); + } + }, [createDialog.open, api?.setDefaultValues]); + + const getNestedValue = (obj, path) => { + return path + .split(".") + .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); + }; + // Handling link navigation if (api.link) { - const getNestedValue = (obj, path) => { - return path - .split(".") - .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); - }; - const linkWithRowData = api.link.replace(/\[([^\]]+)\]/g, (_, key) => { return getNestedValue(row, key) || `[${key}]`; }); @@ -240,17 +299,35 @@ export const CippApiDialog = (props) => { setPartialResults([]); }; + var confirmText; + if (typeof api?.confirmText === "string" && !Array.isArray(row)) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { + return getNestedValue(row, key) || `[${key}]`; + }); + } else if (Array.isArray(row) && row.length > 1) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, "the selected rows"); + } else if (Array.isArray(row) && row.length === 1) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, (_, key) => { + return getNestedValue(row[0], key) || `[${key}]`; + }); + } else { + confirmText = api.confirmText; + } + return (
{title} - {api.confirmText} + {confirmText} {fields && fields.map((fieldProps, index) => { + if (fieldProps?.api?.processFieldData) { + fieldProps.api.data = processActionData(fieldProps.api.data, row); + } return ( { - diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index b1fee51f09de..4d0180f485df 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -1,16 +1,30 @@ -import { Close, ContentCopy } from "@mui/icons-material"; -import { Alert, CircularProgress, Collapse, IconButton, Typography } from "@mui/material"; -import { useEffect, useState, useMemo } from "react"; +import { Close, Download, RouterOutlined } from "@mui/icons-material"; +import { + Alert, + CircularProgress, + Collapse, + IconButton, + Stack, + Typography, + Box, + SvgIcon, + Tooltip, +} from "@mui/material"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { getCippError } from "../../utils/get-cipp-error"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; -import { Grid } from "@mui/system"; +import React from "react"; +import { CippTableDialog } from "./CippTableDialog"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { useDialog } from "../../hooks/use-dialog"; +import { useRouter } from "next/router"; const extractAllResults = (data) => { const results = []; const getSeverity = (text) => { if (typeof text !== "string") return "success"; - return /error|failed|exception|not found/i.test(text) ? "error" : "success"; + return /error|failed|exception|not found|invalid_grant/i.test(text) ? "error" : "success"; }; const processResultItem = (item) => { @@ -52,40 +66,47 @@ const extractAllResults = (data) => { return; } - const ignoreKeys = ["metadata", "Metadata"]; + if (obj?.resultText) { + const processed = processResultItem(obj); + if (processed) { + results.push(processed); + } + } else { + const ignoreKeys = ["metadata", "Metadata"]; - if (typeof obj === "object") { - Object.keys(obj).forEach((key) => { - const value = obj[key]; - if (ignoreKeys.includes(key)) return; - if (["Results", "Result", "results", "result"].includes(key)) { - if (Array.isArray(value)) { - value.forEach((valItem) => { - const processed = processResultItem(valItem); + if (typeof obj === "object") { + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (ignoreKeys.includes(key)) return; + if (["Results", "Result", "results", "result"].includes(key)) { + if (Array.isArray(value)) { + value.forEach((valItem) => { + const processed = processResultItem(valItem); + if (processed) { + results.push(processed); + } else { + extractFrom(valItem); + } + }); + } else if (typeof value === "object") { + const processed = processResultItem(value); if (processed) { results.push(processed); } else { - extractFrom(valItem); + extractFrom(value); } - }); - } else if (typeof value === "object") { - const processed = processResultItem(value); - if (processed) { - results.push(processed); - } else { - extractFrom(value); + } else if (typeof value === "string") { + results.push({ + text: value, + copyField: value, + severity: getSeverity(value), + }); } - } else if (typeof value === "string") { - results.push({ - text: value, - copyField: value, - severity: getSeverity(value), - }); + } else { + extractFrom(value); } - } else { - extractFrom(value); - } - }); + }); + } } }; @@ -99,6 +120,9 @@ export const CippApiResults = (props) => { const [errorVisible, setErrorVisible] = useState(false); const [fetchingVisible, setFetchingVisible] = useState(false); const [finalResults, setFinalResults] = useState([]); + const tableDialog = useDialog(); + const router = useRouter(); + const pageTitle = `${document.title} - Results`; const correctResultObj = useMemo(() => { if (!apiObject.isSuccess) return; @@ -129,7 +153,7 @@ export const CippApiResults = (props) => { const allResults = useMemo(() => { const apiResults = extractAllResults(correctResultObj); return apiResults; - }, [apiObject]); + }, [correctResultObj]); useEffect(() => { setErrorVisible(!!apiObject.isError); @@ -163,16 +187,37 @@ export const CippApiResults = (props) => { errorsOnly, ]); - const handleCloseResult = (id) => { + const handleCloseResult = useCallback((id) => { setFinalResults((prev) => prev.map((r) => (r.id === id ? { ...r, visible: false } : r))); - }; + }, []); + + const handleDownloadCsv = useCallback(() => { + if (!finalResults?.length) return; + + const baseName = document.title.toLowerCase().replace(/[^a-z0-9]/g, "-"); + const fileName = `${baseName}-results.csv`; + + const headers = Object.keys(finalResults[0]); + const rows = finalResults.map((item) => + headers.map((header) => `"${item[header] || ""}"`).join(",") + ); + const csvContent = [headers.join(","), ...rows].join("\n"); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, [finalResults, apiObject]); const hasVisibleResults = finalResults.some((r) => r.visible); return ( - <> + {/* Loading alert */} {!errorsOnly && ( - + { )} {/* Error alert */} - + {apiObject.isError && ( { {/* Individual result alerts */} {apiObject.isSuccess && !errorsOnly && hasVisibleResults && ( - + <> {finalResults.map((resultObj) => ( - - + + { {resultObj.text} - + ))} - + + )} + {(apiObject.isSuccess || apiObject.isError) && finalResults?.length > 0 ? ( + + + tableDialog.handleOpen()}> + + + + + + + + + + + + ) : null} + {tableDialog.open && ( + )} - + ); }; diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 8ecfd6aee8cd..94b71886d005 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -1,9 +1,53 @@ import { ArrowDropDown } from "@mui/icons-material"; -import { Autocomplete, CircularProgress, createFilterOptions, TextField } from "@mui/material"; -import { useEffect, useState } from "react"; +import { + Autocomplete, + CircularProgress, + createFilterOptions, + TextField, + IconButton, +} from "@mui/material"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; import { ApiGetCallWithPagination } from "../../api/ApiCall"; +import { Sync } from "@mui/icons-material"; +import { Stack } from "@mui/system"; +import React from "react"; + +const MemoTextField = React.memo(function MemoTextField({ + params, + label, + placeholder, + ...otherProps +}) { + const { InputProps, ...otherParams } = params; + + return ( + + ); +}); export const CippAutoComplete = (props) => { const { @@ -24,6 +68,7 @@ export const CippAutoComplete = (props) => { required = false, isFetching = false, sx, + removeOptions = [], ...other } = props; @@ -128,6 +173,14 @@ export const CippAutoComplete = (props) => { } }, [api, actionGetRequest.data, actionGetRequest.isSuccess, actionGetRequest.isError]); + const memoizedOptions = useMemo(() => { + let finalOptions = api ? usedOptions : options; + if (removeOptions && removeOptions.length) { + finalOptions = finalOptions.filter((o) => !removeOptions.includes(o.value)); + } + return finalOptions; + }, [api, usedOptions, options, removeOptions]); + const rand = Math.random().toString(36).substring(5); return ( @@ -135,7 +188,7 @@ export const CippAutoComplete = (props) => { key={`${defaultValue}-${rand}`} disabled={disabled || actionGetRequest.isFetching || isFetching} popupIcon={ - actionGetRequest.isFetching ? ( + actionGetRequest.isFetching || isFetching ? ( ) : ( @@ -147,6 +200,7 @@ export const CippAutoComplete = (props) => { disableClearable={disableClearable} multiple={multiple} fullWidth + placeholder={placeholder} filterOptions={(options, params) => { const filtered = filter(options, params); const isExisting = @@ -154,7 +208,6 @@ export const CippAutoComplete = (props) => { options.some( (option) => params.inputValue === option.value || params.inputValue === option.label ); - if (params.inputValue !== "" && creatable && !isExisting) { filtered.push({ label: `Add option: "${params.inputValue}"`, @@ -187,6 +240,9 @@ export const CippAutoComplete = (props) => { } return item; }); + newValue = newValue.filter( + (item) => item.value && item.value !== "" && item.value !== "error" && item.value !== -1 + ); } else { if (newValue?.manual || !newValue?.label) { newValue = { @@ -197,28 +253,45 @@ export const CippAutoComplete = (props) => { onCreateOption(newValue, newValue?.addedFields); } } + if (!newValue?.value || newValue.value === "error") { + newValue = null; + } } if (onChange) { onChange(newValue, newValue?.addedFields); } }} - options={api ? usedOptions : options} - getOptionLabel={(option) => - option - ? option.label === null - ? "" - : option.label || "Label not found - Are you missing a labelField?" - : "" - } + options={memoizedOptions} + getOptionLabel={useCallback( + (option) => + option + ? option.label === null + ? "" + : option.label || "Label not found - Are you missing a labelField?" + : "", + [] + )} sx={sx} renderInput={(params) => ( - + + + {api?.url && api?.showRefresh && ( + { + actionGetRequest.refetch(); + }} + > + + + )} + )} {...other} /> diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx index e23cc9c2557e..a0f8bd137318 100644 --- a/src/components/CippComponents/CippCodeBlock.jsx +++ b/src/components/CippComponents/CippCodeBlock.jsx @@ -25,7 +25,7 @@ const CodeContainer = styled("div")` export const CippCodeBlock = (props) => { const { code, - language = "javascript", + language = "json", showLineNumbers = false, startingLineNumber = 1, wrapLongLines = true, @@ -47,7 +47,7 @@ export const CippCodeBlock = (props) => { {type === "editor" && ( { )} ); -}; +}; \ No newline at end of file diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index aa9329e66f85..c353ac89f71b 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -24,12 +24,17 @@ import { } from "mui-tiptap"; import StarterKit from "@tiptap/starter-kit"; import { CippDataTable } from "../CippTable/CippDataTable"; +import React from "react"; // Helper function to convert bracket notation to dot notation const convertBracketsToDots = (name) => { return name.replace(/\[(\d+)\]/g, ".$1"); // Replace [0] with .0 }; +const MemoizedCippAutoComplete = React.memo((props) => { + return ; +}); + export const CippFormComponent = (props) => { const { validators, @@ -234,14 +239,14 @@ export const CippFormComponent = (props) => { control={formControl.control} rules={validators} render={({ field }) => ( - field.onChange(value.value)} + onChange={(value) => field.onChange(value?.value)} /> )} /> @@ -261,7 +266,7 @@ export const CippFormComponent = (props) => { control={formControl.control} rules={validators} render={({ field }) => ( - { if (watcher.includes(compareValue)) { return children; } - } else if (typeof watcher === "object" && compareValue in watcher) { + } else if (typeof watcher === "object" && watcher !== null && compareValue in watcher) { // Check if object contains the key return children; } @@ -66,6 +66,9 @@ export const CippFormCondition = (props) => { if (!watcher.includes(compareValue)) { return children; } + //extra elsseif; if the value is undefined or null, return children because it does not contain the compareValue + } else if (watcher === undefined || watcher === null) { + return children; } else if (typeof watcher === "object" && !(compareValue in watcher)) { // Check if object does not contain the key return children; diff --git a/src/components/CippComponents/CippFormLicenseSelector.jsx b/src/components/CippComponents/CippFormLicenseSelector.jsx index e435c1b467e1..0f8e1a6b867b 100644 --- a/src/components/CippComponents/CippFormLicenseSelector.jsx +++ b/src/components/CippComponents/CippFormLicenseSelector.jsx @@ -24,14 +24,11 @@ export const CippFormLicenseSelector = ({ api={{ addedField: addedField, tenantFilter: userSettingsDefaults.currentTenant ?? undefined, - url: "/api/ListGraphRequest", - dataKey: "Results", + url: "/api/ListLicenses", labelField: (option) => - `${getCippLicenseTranslation([option])} (${ - option.prepaidUnits.enabled - option.consumedUnits - } available)`, + `${getCippLicenseTranslation([option])} (${option?.availableUnits} available)`, valueField: "skuId", - queryKey: `ListLicenses-${userSettingsDefaults.currentTenant ?? undefined}`, + queryKey: `ListLicenses-${userSettingsDefaults?.currentTenant ?? undefined}`, data: { Endpoint: "subscribedSkus", $count: true, diff --git a/src/components/CippComponents/CippFormTenantSelector.jsx b/src/components/CippComponents/CippFormTenantSelector.jsx index 032d4d6ad96f..fdc3ab1264d5 100644 --- a/src/components/CippComponents/CippFormTenantSelector.jsx +++ b/src/components/CippComponents/CippFormTenantSelector.jsx @@ -3,12 +3,14 @@ import { CippFormComponent } from "./CippFormComponent"; export const CippFormTenantSelector = ({ formControl, + componentType='autoComplete', allTenants = false, type = "multiple", name = "tenantFilter", valueField = "defaultDomainName", required = true, disableClearable = true, + removeOptions = [], ...other }) => { const validators = () => { @@ -22,7 +24,7 @@ export const CippFormTenantSelector = ({ return ( ); diff --git a/src/components/CippComponents/CippSpeedDial.jsx b/src/components/CippComponents/CippSpeedDial.jsx new file mode 100644 index 000000000000..4313ace7a7ef --- /dev/null +++ b/src/components/CippComponents/CippSpeedDial.jsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect } from "react"; +import { + SpeedDial, + SpeedDialAction, + SpeedDialIcon, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Snackbar, + Alert, + CircularProgress, +} from "@mui/material"; +import { Close as CloseIcon } from "@mui/icons-material"; +import { useForm } from "react-hook-form"; +import { CippFormComponent } from "../../components/CippComponents/CippFormComponent"; + +const CippSpeedDial = ({ + actions = [], + position = { bottom: 16, right: 16 }, + icon, + openIcon = , +}) => { + const [openDialogs, setOpenDialogs] = useState({}); + const [loading, setLoading] = useState(false); + const [showSnackbar, setShowSnackbar] = useState(false); + const [speedDialOpen, setSpeedDialOpen] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + + const formControls = actions.reduce((acc, action) => { + if (action.form) { + acc[action.id] = useForm({ + mode: "onChange", + defaultValues: action.form.defaultValues || {}, + }); + } + return acc; + }, {}); + + const handleSpeedDialClose = () => { + if (!isHovering) { + setTimeout(() => { + setSpeedDialOpen(false); + }, 200); + } + }; + + const handleMouseEnter = () => { + setIsHovering(true); + setSpeedDialOpen(true); + }; + + const handleMouseLeave = () => { + setIsHovering(false); + handleSpeedDialClose(); + }; + + const handleDialogOpen = (actionId) => { + setOpenDialogs((prev) => ({ ...prev, [actionId]: true })); + }; + + const handleDialogClose = (actionId) => { + setOpenDialogs((prev) => ({ ...prev, [actionId]: false })); + }; + + const handleSubmit = async (actionId, data) => { + if (!actions.find((a) => a.id === actionId)?.onSubmit) return; + + setLoading(true); + try { + const action = actions.find((a) => a.id === actionId); + const result = await action.onSubmit(data); + + if (result.success) { + formControls[actionId]?.reset(); + handleDialogClose(actionId); + } + setSnackbarMessage(result.message); + setShowSnackbar(true); + } catch (error) { + console.error(`Error submitting ${actionId}:`, error); + setSnackbarMessage("An error occurred while submitting"); + setShowSnackbar(true); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (speedDialOpen) { + const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]'); + if (speedDial && !speedDial.contains(event.target)) { + setSpeedDialOpen(false); + } + } + }; + + document.addEventListener("click", handleClickOutside); + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, [speedDialOpen]); + + return ( + <> + } + open={speedDialOpen} + onClose={handleSpeedDialClose} + onOpen={() => setSpeedDialOpen(true)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + {actions.map((action) => ( + { + if (action.form) { + handleDialogOpen(action.id); + } else if (action.onClick) { + action.onClick(); + } + setSpeedDialOpen(false); + }} + tooltipOpen + sx={{ + "&.MuiSpeedDialAction-fab": { + backgroundColor: "background.paper", + "&:hover": { + backgroundColor: "action.hover", + }, + }, + "& .MuiSpeedDialAction-staticTooltipLabel": { + cursor: "pointer", + whiteSpace: "nowrap", + marginRight: "10px", + padding: "6px 10px", + "&:hover": { + backgroundColor: "action.hover", + }, + }, + }} + /> + ))} + + + {actions + .filter((action) => action.form) + .map((action) => ( + handleDialogClose(action.id)} + maxWidth="md" + fullWidth + > + {action.form.title} + + + + + + + + + ))} + + setShowSnackbar(false)} + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + > + setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}> + {snackbarMessage} + + + + ); +}; + +export default CippSpeedDial; diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index 0876bded5bd2..fef71fa8ccb0 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -44,4 +44,6 @@ export const CippTranslations = { "commitmentTerm.renewalConfiguration.renewalDate": "Renewal Date", storageUsedInBytes: "Storage Used", prohibitSendReceiveQuotaInBytes: "Quota", + ClientId: "Client ID", + html_url: "URL", }; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index fe532d259f4b..93e6d991adb6 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -98,6 +98,7 @@ export const CippUserActions = () => { { label: "Disabled", value: "Disabled" }, ], multiple: false, + creatable: false, }, ], confirmText: "Are you sure you want to set per-user MFA for these users?", @@ -283,7 +284,7 @@ export const CippUserActions = () => { }, { label: "Clear Immutable ID", - type: "GET", + type: "POST", icon: , url: "/api/ExecClrImmId", data: { @@ -291,7 +292,7 @@ export const CippUserActions = () => { }, confirmText: "Are you sure you want to clear the Immutable ID for this user?", multiPost: false, - condition: (row) => row.onPremisesSyncEnabled, + condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId, }, { label: "Revoke all user sessions", diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 991ade01854a..f772ae0e8e0e 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -8,6 +8,8 @@ import { CippFormLicenseSelector } from "/src/components/CippComponents/CippForm import Grid from "@mui/material/Grid"; import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; +import { useWatch } from "react-hook-form"; +import { useEffect } from "react"; const CippAddEditUser = (props) => { const { formControl, userSettingsDefaults, formType = "add" } = props; @@ -16,6 +18,15 @@ const CippAddEditUser = (props) => { url: "/api/ListExtensionsConfig", queryKey: "ListExtensionsConfig", }); + + const watcher = useWatch({ control: formControl.control }); + useEffect(() => { + //if watch.firstname changes, and watch.lastname changes, set displayname to firstname + lastname + if (watcher.givenName && watcher.surname && formType === "add") { + formControl.setValue("displayName", `${watcher.givenName} ${watcher.surname}`); + } + }, [watcher.givenName, watcher.surname]); + return ( diff --git a/src/components/CippFormPages/CippAddGroupForm.jsx b/src/components/CippFormPages/CippAddGroupForm.jsx index 9745d9858636..daac5f7aef7f 100644 --- a/src/components/CippFormPages/CippAddGroupForm.jsx +++ b/src/components/CippFormPages/CippAddGroupForm.jsx @@ -51,7 +51,7 @@ const CippAddGroupForm = (props) => { formControl={formControl} name="owners" label="Owners" - multiple={false} + multiple={true} select={"id,userPrincipalName,displayName"} /> @@ -62,7 +62,7 @@ const CippAddGroupForm = (props) => { formControl={formControl} name="members" label="Members" - multiple={false} + multiple={true} select={"id,userPrincipalName,displayName"} /> @@ -76,7 +76,7 @@ const CippAddGroupForm = (props) => { { label: "Security Group", value: "generic" }, { label: "Microsoft 365 Group", value: "m365" }, { label: "Dynamic Group", value: "dynamic" }, - { label: "Dynamic Distribution Group", value: "dynamicdistribution" }, + { label: "Dynamic Distribution Group", value: "dynamicDistribution" }, { label: "Distribution List", value: "distribution" }, { label: "Mail Enabled Security Group", value: "security" }, ]} diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index e46b0ab157f6..66d8bb4b6ecb 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -257,7 +257,7 @@ const CippExchangeSettingsForm = (props) => { { - + {section.formContent} diff --git a/src/components/CippFormPages/CippFormPage.jsx b/src/components/CippFormPages/CippFormPage.jsx index 585975b073bb..baae183502a4 100644 --- a/src/components/CippFormPages/CippFormPage.jsx +++ b/src/components/CippFormPages/CippFormPage.jsx @@ -27,23 +27,23 @@ const CippFormPage = (props) => { formControl, postUrl, customDataformatter, - resetForm = true, + resetForm = false, hideBackButton = false, hidePageType = false, hideTitle = false, hideSubmit = false, + allowResubmit = false, addedButtons, ...other } = props; const router = useRouter(); - //check if there are const postCall = ApiPostCall({ datafromUrl: true, relatedQueryKeys: queryKey, }); - const { isValid } = useFormState({ control: formControl.control }); + const { isValid, isDirty } = useFormState({ control: formControl.control }); useEffect(() => { delete router.query.tenantFilter; @@ -134,7 +134,7 @@ const CippFormPage = (props) => { {addedButtons && addedButtons} + + + - + + ); }; diff --git a/src/components/CippIntegrations/CippApiClientManagement.jsx b/src/components/CippIntegrations/CippApiClientManagement.jsx new file mode 100644 index 000000000000..71b60116dd50 --- /dev/null +++ b/src/components/CippIntegrations/CippApiClientManagement.jsx @@ -0,0 +1,419 @@ +import { Button, Stack, SvgIcon, Menu, MenuItem, ListItemText, Alert } from "@mui/material"; +import { useState } from "react"; +import isEqual from "lodash/isEqual"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "/src/api/ApiCall"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { + ChevronDownIcon, + ClipboardDocumentIcon, + PencilIcon, + PlusSmallIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { CippApiDialog } from "../CippComponents/CippApiDialog"; +import { Create, Key, Save, Sync } from "@mui/icons-material"; +import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; +import { Box } from "@mui/system"; + +const CippApiClientManagement = () => { + const [openAddClientDialog, setOpenAddClientDialog] = useState(false); + const [openAddExistingAppDialog, setOpenAddExistingAppDialog] = useState(false); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + + const formControl = useForm({ + mode: "onChange", + }); + + const postCall = ApiPostCall({ + datafromUrl: true, + relatedQueryKeys: ["ApiClients", "AzureConfiguration"], + }); + + const azureConfig = ApiGetCall({ + url: "/api/ExecApiClient", + data: { Action: "GetAzureConfiguration" }, + queryKey: "AzureConfiguration", + }); + + const apiClients = ApiGetCallWithPagination({ + url: "/api/ExecApiClient", + data: { Action: "List" }, + queryKey: "ApiClients", + }); + + const handleMenuOpen = (event) => { + setMenuAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; + + const handleSaveToAzure = () => { + postCall.mutate({ + url: `/api/ExecApiClient?action=SaveToAzure`, + data: {}, + }); + handleMenuClose(); + }; + + const actions = [ + { + label: "Edit", + icon: ( + + + + ), + confirmText: "Update the API client settings for [AppName]?", + hideBulk: true, + setDefaultValues: true, + fields: [ + { + type: "autoComplete", + name: "Role", + multiple: false, + creatable: false, + label: "Select Role", + placeholder: "Choose a role from the Custom Role list.", + api: { + url: "/api/ListCustomRole", + queryKey: "CustomRoleList", + labelField: "RowKey", + valueField: "RowKey", + showRefresh: true, + }, + }, + { + type: "autoComplete", + name: "IPRange", + multiple: true, + freeSolo: true, + creatable: true, + options: [], + label: "Enter IP Range (Single hosts or CIDR notation)", + placeholder: "Type in the IP addresses and hit enter.", + }, + { + type: "switch", + name: "Enabled", + label: "Enable this client", + }, + ], + type: "POST", + url: "/api/ExecApiClient", + data: { + Action: "AddUpdate", + ClientId: "ClientId", + }, + relatedQueryKeys: ["ApiClients"], + }, + { + label: "Reset Application Secret", + icon: , + confirmText: "Are you sure you want to reset the application secret for [AppName]?", + type: "POST", + url: "/api/ExecApiClient", + data: { + Action: "ResetSecret", + ClientId: "ClientId", + }, + hideBulk: true, + }, + { + label: "Copy API Scope", + icon: , + noConfirm: true, + customFunction: (row, action, formData) => { + var scope = `api://${row.ClientId}/.default`; + navigator.clipboard.writeText(scope); + }, + hideBulk: true, + }, + { + label: "Delete Client", + icon: , + confirmText: "Are you sure you want to delete [AppName]?", + type: "POST", + url: "/api/ExecApiClient", + data: { + Action: "Delete", + ClientId: "ClientId", + }, + fields: [ + { + type: "switch", + name: "RemoveAppReg", + label: "Remove App Registration", + }, + ], + relatedQueryKeys: ["ApiClients"], + multiPost: false, + }, + ]; + + return ( + <> + + + + + { + handleMenuClose(); + setOpenAddClientDialog(true); + }} + > + + + + Create New Client + + { + handleMenuClose(); + setOpenAddExistingAppDialog(true); + }} + > + + + + Add Existing Client + + { + azureConfig.refetch(); + handleMenuClose(); + }} + > + + + + Refresh Configuration + + + + + + Save to Azure + + + + } + propertyItems={[ + { + label: "Microsoft Authentication Enabled", + value: azureConfig.data?.Results?.Enabled, + }, + { + label: "API Url", + value: azureConfig.data?.Results?.ApiUrl ? ( + + ) : ( + "Not Available" + ), + }, + { + label: "Token URL", + value: azureConfig.data?.Results?.TenantID ? ( + + ) : ( + "Not Available" + ), + }, + { + label: "Tenant ID", + value: azureConfig.data?.Results?.TenantID ? ( + + ) : ( + "Not Available" + ), + }, + ]} + layout="dual" + showDivider={false} + isFetching={azureConfig.isFetching} + /> + {azureConfig.isSuccess && azureConfig.data?.Results?.ClientIDs && ( + <> + {!isEqual( + apiClients.data?.pages?.[0]?.Results?.filter((c) => c.Enabled) + .map((c) => c.ClientId) + .sort(), + azureConfig.data?.Results?.ClientIDs?.sort() + ) && ( + + + You have unsaved changes. Click Actions > Save Azure Configuration to update + the allowed API Clients. + + + )} + + )} + {azureConfig.isSuccess && azureConfig.data?.Results?.Enabled === false && ( + + + Microsoft Authentication is disabled. Configure API Clients and click Actions > + Save Azure Configuration. + + + )} + + + + + + + setOpenAddClientDialog(false), + }} + title="Add Client" + fields={[ + { + type: "textField", + name: "AppName", + label: "App Name", + placeholder: "Enter a name for this Application Registration.", + }, + { + type: "autoComplete", + name: "Role", + multiple: false, + creatable: false, + label: "Select Role", + api: { + url: "/api/ListCustomRole", + queryKey: "CustomRoleList", + labelField: "RowKey", + valueField: "RowKey", + showRefresh: true, + }, + placeholder: "Choose a role from the Custom Role list.", + }, + { + type: "autoComplete", + name: "IPRange", + multiple: true, + freeSolo: true, + creatable: true, + options: [], + label: "Enter IP Ranges (Single hosts or CIDR notation)", + placeholder: "Type in the IP addresses and hit enter.", + }, + { + type: "switch", + name: "Enabled", + label: "Enable this client", + }, + ]} + api={{ + type: "POST", + url: "/api/ExecApiClient", + data: { Action: "AddUpdate" }, + relatedQueryKeys: [`ApiClients`], + }} + /> + setOpenAddExistingAppDialog(false), + }} + title="Add Existing App" + fields={[ + { + type: "autoComplete", + name: "ClientId", + label: "Existing App", + placeholder: "Select an existing API application.", + api: { + type: "GET", + url: "/api/ExecApiClient", + data: { Action: "ListAvailable" }, + queryKey: `AvailableApiApps`, + dataKey: "Results", + labelField: (app) => `${app.displayName} (${app.appId})`, + valueField: "appId", + addedField: { + displayName: "displayName", + createdDateTime: "createdDateTime", + }, + showRefresh: true, + }, + creatable: false, + multiple: false, + }, + { + type: "autoComplete", + name: "Role", + multiple: false, + creatable: false, + label: "Select Role", + placeholder: "Choose a role from the Custom Role list.", + api: { + url: "/api/ListCustomRole", + queryKey: "CustomRoleList", + labelField: "RowKey", + valueField: "RowKey", + showRefresh: true, + }, + }, + { + type: "autoComplete", + name: "IPRange", + multiple: true, + freeSolo: true, + creatable: true, + options: [], + label: "Enter IP Ranges (Single hosts or CIDR notation)", + placeholder: "Type in the IP addresses and hit enter.", + }, + { + type: "switch", + name: "Enabled", + label: "Enable this client", + }, + ]} + api={{ + type: "POST", + url: "/api/ExecApiClient", + data: { Action: "!AddUpdate" }, + relatedQueryKeys: [`ApiClients`], + }} + /> + + ); +}; + +export default CippApiClientManagement; diff --git a/src/components/CippIntegrations/CippIntegrationSettings.jsx b/src/components/CippIntegrations/CippIntegrationSettings.jsx index 53334b6ada59..07731168d0be 100644 --- a/src/components/CippIntegrations/CippIntegrationSettings.jsx +++ b/src/components/CippIntegrations/CippIntegrationSettings.jsx @@ -1,4 +1,5 @@ -import { Box, Grid } from "@mui/material"; +import { Box } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormSection from "/src/components/CippFormPages/CippFormSection"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useForm } from "react-hook-form"; @@ -6,7 +7,8 @@ import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall"; import { useRouter } from "next/router"; import extensions from "/src/data/Extensions.json"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; const CippIntegrationSettings = ({ children }) => { const router = useRouter(); @@ -43,7 +45,7 @@ const CippIntegrationSettings = ({ children }) => { <> {integrations.isSuccess && extension ? ( { {extension.SettingOptions.map((setting, index) => ( - - - - - + + {setting?.condition ? ( + + + + + + + + ) : ( + + + + + + )} + ))} diff --git a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx index 8f1ecc67ec5c..179f057db2d2 100644 --- a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx +++ b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx @@ -85,7 +85,7 @@ const CippIntegrationSettings = ({ children }) => { if (tableData?.find((item) => item.TenantId === selectedTenant.addedFields.customerId)) return; const newRowData = { - TenantId: selectedTenant.addedFields.customerId, + TenantId: selectedTenant.value, Tenant: selectedTenant.label, IntegrationName: selectedCompany.label, IntegrationId: selectedCompany.value, @@ -95,37 +95,37 @@ const CippIntegrationSettings = ({ children }) => { }; const handleAutoMap = () => { - const newTableData = []; - tenantList.data?.pages[0]?.forEach((tenant) => { - const matchingCompany = mappings.data.Companies.find( - (company) => company.name === tenant.displayName - ); - if ( - Array.isArray(tableData) && - tableData?.find((item) => item.TenantId === tenant.customerId) - ) - return; - if (matchingCompany) { - newTableData.push({ - TenantId: tenant.customerId, - Tenant: tenant.displayName, - IntegrationName: matchingCompany.name, - IntegrationId: matchingCompany.value, - }); - } - }); - if (Array.isArray(tableData)) { - setTableData([...tableData, ...newTableData]); - } else { - setTableData(newTableData); - } - if (extension.autoMapSyncApi) { - automapPostCall.mutate({ - url: `/api/ExecExtensionMapping?AutoMapping=${router.query.id}`, - queryKey: `IntegrationTenantMapping-${router.query.id}`, + const newTableData = []; + tenantList.data?.pages[0]?.forEach((tenant) => { + const matchingCompany = mappings.data.Companies.find( + (company) => company.name === tenant.displayName + ); + if ( + Array.isArray(tableData) && + tableData?.find((item) => item.TenantId === tenant.customerId) + ) + return; + if (matchingCompany) { + newTableData.push({ + TenantId: tenant.customerId, + Tenant: tenant.displayName, + IntegrationName: matchingCompany.name, + IntegrationId: matchingCompany.value, }); } - }; + }); + if (Array.isArray(tableData)) { + setTableData([...tableData, ...newTableData]); + } else { + setTableData(newTableData); + } + if (extension.autoMapSyncApi) { + automapPostCall.mutate({ + url: `/api/ExecExtensionMapping?AutoMapping=${router.query.id}`, + queryKey: `IntegrationTenantMapping-${router.query.id}`, + }); + } + }; const actions = [ { @@ -140,7 +140,7 @@ const CippIntegrationSettings = ({ children }) => { useEffect(() => { if (mappings.isSuccess) { - setTableData(mappings.data.Mappings); + setTableData(mappings.data.Mappings ?? []); } }, [mappings.isSuccess]); @@ -167,6 +167,8 @@ const CippIntegrationSettings = ({ children }) => { multiple={false} required={false} disableClearable={false} + removeOptions={tableData.map((item) => item.TenantId)} + valueField="customerId" /> diff --git a/src/components/CippSettings/CippCustomRoles.jsx b/src/components/CippSettings/CippCustomRoles.jsx index 6387ae339a4f..0dbf589a75f5 100644 --- a/src/components/CippSettings/CippCustomRoles.jsx +++ b/src/components/CippSettings/CippCustomRoles.jsx @@ -67,10 +67,7 @@ export const CippCustomRoles = () => { queryKey: "customRoleList", }); - const { - data: { pages = [] } = {}, - isSuccess: tenantsSuccess, - } = ApiGetCallWithPagination({ + const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ url: "/api/ListTenants?AllTenantSelector=true", queryKey: "ListTenants-AllTenantSelector", }); @@ -280,7 +277,7 @@ export const CippCustomRoles = () => { label: role.RowKey, value: role.RowKey, }))} - isLoading={customRoleListFetching} + isFetching={customRoleListFetching} refreshFunction={() => refetchCustomRoleList()} creatable={true} formControl={formControl} @@ -289,8 +286,9 @@ export const CippCustomRoles = () => { /> {cippApiRoleSelected && ( - This role will limit access for the CIPP-API integration. It is not intended to be - used for users. + This is the default role for all API clients in the CIPP-API integration. If you + would like different permissions for specific applications, create a role per + application and select it from the CIPP-API integrations page. )}
diff --git a/src/components/CippSettings/CippPermissionCheck.jsx b/src/components/CippSettings/CippPermissionCheck.jsx index 5ad0f5d0a502..db0c81c7ad38 100644 --- a/src/components/CippSettings/CippPermissionCheck.jsx +++ b/src/components/CippSettings/CippPermissionCheck.jsx @@ -142,7 +142,7 @@ const CippPermissionCheck = (props) => { {(executeCheck.isSuccess || executeCheck.isLoading) && ( <> {executeCheck.data?.Metadata?.AlertMessage && ( - + - + diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index 2fb081155944..69b014d723e4 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -42,121 +42,142 @@ const CippStandardDialog = ({ const handleSearchQueryChange = useCallback( debounce((query) => { - setSearchQuery(query); + setSearchQuery(query.trim()); }, 50), [] ); return ( - + Select a Standard to Add handleSearchQueryChange(e.target.value.toLowerCase())} /> - - {Object.keys(categories).map((category) => - filterStandards(categories[category]).map((standard) => ( - - - - - {standard.label} - - {standard.helpText && ( - <> - - Description: - - - {standard.helpText} - - - )} - - Category: - - - {standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > - 0 && ( - <> - - Tags: - - - {standard.tag - .filter((tag) => !tag.toLowerCase().includes("impact")) - .map((tag, idx) => ( - - ))} - - - )} - - Impact: - - - {standard.recommendedBy?.length > 0 && ( - <> - - Recommended By: - - - {standard.recommendedBy.join(", ")} - - - )} - - - - {standard.multiple ? ( - handleAddClick(standard.name)} - > - - - ) : ( - handleToggleSingleStandard(standard.name)} - /> + + {Object.keys(categories).every( + (category) => filterStandards(categories[category]).length === 0 + ) ? ( + + Search returned no results + + ) : ( + Object.keys(categories).map((category) => + filterStandards(categories[category]).map((standard) => ( + + + + + {standard.label} + + {standard.helpText && ( + <> + + Description: + + + {standard.helpText} + + + )} + + Category: + + + {standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > + 0 && ( + <> + + Tags: + + + {standard.tag + .filter((tag) => !tag.toLowerCase().includes("impact")) + .map((tag, idx) => ( + + ))} + + + )} + + Impact: + + - )} - - - - )) + {standard.recommendedBy?.length > 0 && ( + <> + + Recommended By: + + + {standard.recommendedBy.join(", ")} + + + )} + + + + {standard.multiple ? ( + handleAddClick(standard.name)} + > + + + ) : ( + handleToggleSingleStandard(standard.name)} + /> + } + label="Add this standard to the template" + /> + )} + + + + )) + ) )} diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index c6a405de636f..2c87c7562fe8 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -1,14 +1,5 @@ import PropTypes from "prop-types"; -import { - Card, - CardContent, - CardHeader, - Divider, - formControlLabelClasses, - Stack, - SvgIcon, - Typography, -} from "@mui/material"; +import { Card, CardContent, CardHeader, Divider, Stack, SvgIcon, Typography } from "@mui/material"; import { styled } from "@mui/material/styles"; import { Timeline, @@ -24,7 +15,7 @@ import { ActionListItem } from "/src/components/action-list-item"; import CheckIcon from "@heroicons/react/24/outline/CheckIcon"; import CloseIcon from "@mui/icons-material/Close"; import { useWatch } from "react-hook-form"; -import { useEffect, useState } from "react"; +import { use, useEffect, useState } from "react"; import _ from "lodash"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "../CippComponents/CippFormTenantSelector"; @@ -127,6 +118,15 @@ const CippStandardsSideBar = ({ fullWidth /> + + { + //if usedData changes, deselect all rows + table.toggleAllRowsSelected(false); + }, [usedData]); //if the currentTenant Switches, remove Graph filters useEffect(() => { if (currentTenant) { @@ -81,6 +85,10 @@ export const CIPPTableToptoolbar = ({ } }, [settings?.columnDefaults?.[pageName], router, usedColumns]); + useEffect(() => { + setOriginalSimpleColumns(simpleColumns); + }, [simpleColumns]); + const presetList = ApiGetCall({ url: "/api/ListGraphExplorerPresets", queryKey: `ListGraphExplorerPresets${api?.data?.Endpoint ?? ""}`, @@ -254,7 +262,7 @@ export const CIPPTableToptoolbar = ({ // update filters to include graph explorer presets setFilterList([...filters, ...graphPresetList]); } - }, [presetList?.isSuccess]); + }, [presetList?.isSuccess, simpleColumns]); return ( <> @@ -442,6 +450,11 @@ export const CIPPTableToptoolbar = ({ + {getRequestData?.data?.pages?.[0].Metadata?.ColdStart === true && ( + + + + )} {actions && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && ( <> + <> + + + {!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( + appId?.data?.applicationId + ) && ( + + The Application ID is not valid. Please return to the first page of the SAM + wizard and use the Manual . + + )} + } > @@ -284,6 +311,13 @@ export const CippDeploymentStep = (props) => { name="TenantID" label="Tenant ID" placeholder="Enter the Tenant ID. Leave blank to retain previous key." + validators={{ + validate: (value) => { + const guidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return value === "" || guidRegex.test(value) || "Invalid Tenant ID"; + }, + }} /> { name="ApplicationID" label="Application ID" placeholder="Enter the Application ID. Leave blank to retain previous key." + validators={{ + validate: (value) => { + const guidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return value === "" || guidRegex.test(value) || "Invalid Application ID"; + }, + }} /> { name="ApplicationSecret" label="Application Secret" placeholder="Enter the application secret. Leave blank to retain previous key." + validators={{ + validate: (value) => { + const secretRegex = /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/; + return ( + value === "" || + secretRegex.test(value) || + "This should be the secret value, not the secret ID" + ); + }, + }} /> { name="RefreshToken" label="Refresh Token" placeholder="Enter the refresh token. Leave blank to retain previous key." + validators={{ + validate: (value) => { + const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; + return value === "" || jwtRegex.test(value) || "Invalid Refresh Token"; + }, + }} /> )} diff --git a/src/components/CippWizard/CippWizardCSVImport.jsx b/src/components/CippWizard/CippWizardCSVImport.jsx index 6f665df4523b..141672f68874 100644 --- a/src/components/CippWizard/CippWizardCSVImport.jsx +++ b/src/components/CippWizard/CippWizardCSVImport.jsx @@ -32,6 +32,11 @@ export const CippWizardCSVImport = (props) => { const [newTableData, setTableData] = useState([]); const [open, setOpen] = useState(false); + // Register form field with validation + formControl.register(name, { + validate: (value) => Array.isArray(value) && value.length > 0, + }); + const handleRemoveItem = (row) => { if (row === undefined) return false; const index = tableData?.findIndex((item) => item === row); @@ -49,7 +54,9 @@ export const CippWizardCSVImport = (props) => { }; useEffect(() => { - formControl.setValue(name, newTableData); + formControl.setValue(name, newTableData, { + shouldValidate: true, + }); }, [newTableData]); const actions = [ diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index 7d69817bcf52..6c79070dcd81 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -5,12 +5,14 @@ import { CippFormCondition } from "../CippComponents/CippFormCondition"; import { useWatch } from "react-hook-form"; import { useEffect, useState } from "react"; import { Grid } from "@mui/system"; +import { useSettings } from "../../hooks/use-settings"; export const CippWizardOffboarding = (props) => { const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props; const currentTenant = formControl.watch("tenantFilter"); const selectedUsers = useWatch({ control: formControl.control, name: "user" }); const [showAlert, setShowAlert] = useState(false); + const userSettingsDefaults = useSettings().userSettingsDefaults; useEffect(() => { if (selectedUsers.length >= 4) { @@ -19,6 +21,14 @@ export const CippWizardOffboarding = (props) => { } }, [selectedUsers]); + useEffect(() => { + if (userSettingsDefaults?.offboardingDefaults) { + userSettingsDefaults.offboardingDefaults.forEach((setting) => { + formControl.setValue(setting.name, setting.value); + }); + } + }, [userSettingsDefaults]); + return ( @@ -87,6 +97,12 @@ export const CippWizardOffboarding = (props) => { type="switch" formControl={formControl} /> + { if (null === roles) { return ; } else { - const isAuthenticated = roles.includes("authenticated") && !error; + const blockedRoles = ["anonymous", "authenticated"]; + const userRoles = roles.filter((role) => !blockedRoles.includes(role)); + const isAuthenticated = userRoles.length > 0 && !error; const isAdmin = roles.includes("admin"); if (routeType === "admin") { return !isAdmin ? : children; diff --git a/src/components/bulk-actions-menu.js b/src/components/bulk-actions-menu.js index b4ceba4c48bd..fd15898e28a3 100644 --- a/src/components/bulk-actions-menu.js +++ b/src/components/bulk-actions-menu.js @@ -2,6 +2,33 @@ import PropTypes from "prop-types"; import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon"; import { Button, Link, ListItemText, Menu, MenuItem, SvgIcon } from "@mui/material"; import { usePopover } from "../hooks/use-popover"; +import { FilePresent, Laptop, Mail, Share, Shield, ShieldMoon } from "@mui/icons-material"; +import { GlobeAltIcon, UsersIcon, ServerIcon } from "@heroicons/react/24/outline"; + +function getIconByName(iconName) { + switch (iconName) { + case "GlobeAltIcon": + return ; + case "Mail": + return ; + case "UsersIcon": + return ; + case "FilePresent": + return ; + case "ServerIcon": + return ; + case "Laptop": + return ; + case "Share": + return ; + case "Shield": + return ; + case "ShieldMoon": + return ; + default: + return null; + } +} export const BulkActionsMenu = (props) => { const { buttonName, sx, row, actions = [], ...other } = props; @@ -55,12 +82,14 @@ export const BulkActionsMenu = (props) => { target="_blank" rel="noreferrer" > + {getIconByName(action.icon)} ); } else { return ( + {getIconByName(action.icon)} ); diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 0110e7d63729..7f9d8ddcb38e 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -7,19 +7,12 @@ "logo": "/assets/integrations/cipp-api.png", "forceSyncButton": false, "hideTestButton": true, - "disableWhenhosted": true, - "description": "Enable the CIPP API to access CIPP data outside of CIPP using a RESTful API.", - "helpText": "This integration allows you to enable CIPP-API access outside of CIPP. Requires Global Administrator permissions inside your tenant for activation of the API. The API credentials will only be shown once.", - "SettingOptions": [ - { - "type": "switch", - "name": "cippapi.ResetPassword", - "label": "Reset application secret - this will invalidate all existing API tokens" - }, + "description": "The CIPP API allows you to access CIPP data outside of CIPP using a RESTful API.", + "helpText": "This integration allows you to enable CIPP-API access outside of CIPP. This is useful for integrations with other systems or custom scripts.", + "links": [ { - "type": "switch", - "name": "cippapi.Enabled", - "label": "Enable Integration" + "name": "CIPP API Documentation", + "url": "https://docs.cipp.app/api-documentation/setup-and-authentication" } ], "mappingRequired": false @@ -35,10 +28,6 @@ "helpText": "This integration allows you to manage your CSP licensing from CIPP. Purchase new licenses while adding or editing users in CIPP. Not a Sherweb partner? Sign up for the Sherweb Cloud Services for MSPs program.", "mappingRequired": true, "links": [ - { - "name": "Register for beta", - "url": "https://go.cyberdrain.com/sherweb" - }, { "name": "Sherweb Cloud Services for MSPs", "url": "https://info.sherweb.com/sherweb-cloud-services-for-msps" @@ -76,7 +65,7 @@ "name": "Sherweb.AllowedCustomRoles", "label": "Select custom roles that are allowed to purchase licenses", "api": { - "url": "/api/ExecCustomRole", + "url": "/api/ListCustomRole", "queryKey": "CustomRoles", "labelField": "RowKey", "valueField": "RowKey" @@ -163,15 +152,25 @@ "type": "textField", "name": "HaloPSA.Tenant", "label": "HaloPSA Tenant", - "placeholder": "Enter your HaloPSA Tenant (Leave blank if self-hosted", + "placeholder": "Enter your HaloPSA Tenant (Leave blank if self-hosted)", "required": true }, { - "type": "textField", + "type": "autoComplete", "name": "HaloPSA.TicketType", - "label": "HaloPSA Ticket Type ID", - "placeholder": "Enter your HaloPSA ticket type to use for CIPP Tickets. Leave blank for default.", - "required": true + "label": "Select your HaloPSA Ticket Type, leave blank for default.", + "multiple": false, + "api": { + "url": "/api/ExecExtensionMapping", + "data": { + "List": "HaloPSAFields" + }, + "queryKey": "HaloTicketTypes", + "dataKey": "TicketTypes", + "labelField": "name", + "valueField": "id", + "showRefresh": true + } }, { "type": "textField", @@ -319,6 +318,17 @@ "type": "switch", "name": "Hudu.MonitorDomains", "label": "Monitor domains in Hudu" + }, + { + "_comment": "I have added this switch as a logic check for the Hudu integration script to check against when CIPP first connects to the Hudu Instance via Connect-HuduAPI.ps1", + "type": "switch", + "name": "Hudu.CFEnabled", + "label": "Connect to HUDU through CloudFlare Tunnel with the Service Account credentials.", + "condition": { + "field": "CFZTNA.Enabled", + "compareType": "is", + "compareValue": true + } } ], "mappingRequired": true, @@ -334,7 +344,7 @@ "logoDark": "/assets/integrations/pwpush_dark.png", "forceSyncButton": false, "description": "Enable the PasswordPusher integration to generate password links instead of plain text passwords.", - "helpText": "This integration allows you to generate password links instead of plain text passwords. Configure authentication and expiration settings that will apply to all generated passwords. If you are a PWPush Pro customer and utilizing custom domains, please do not enter your custom domain in the Base URL field.", + "helpText": "This integration allows you to generate password links instead of plain text passwords. Configure authentication and expiration settings that will apply to all generated passwords. If you are a Hosted PWPush Pro customer you can also select an account for branding if you enable Bearer Authentication. Self-Hosted must use the Email Address and API Key method for authenticated pushes.", "links": [ { "name": "PWPush Documentation", @@ -349,7 +359,12 @@ { "type": "switch", "name": "PWPush.Enabled", - "label": "Enable Integration - Replace passwords with PWPush links" + "label": "Enable Integration" + }, + { + "type": "switch", + "name": "PWPush.UseBearerAuth", + "label": "Use Bearer Authentication (Hosted only)" }, { "type": "textField", @@ -357,12 +372,6 @@ "label": "PWPush URL", "placeholder": "Enter your PWPush URL. (default: https://pwpush.com)" }, - { - "type": "textField", - "name": "PWPush.EmailAddress", - "label": "PWPush email address", - "placeholder": "Enter your email address for PWPush. (optional)" - }, { "type": "password", "name": "PWPush.APIKey", @@ -370,9 +379,35 @@ "placeholder": "Enter your PWPush API Key. (optional)" }, { - "type": "switch", - "name": "PWPush.RetrievalStep", - "label": "Click to retrieve password (recommended)" + "type": "textField", + "name": "PWPush.EmailAddress", + "label": "PWPush email address", + "placeholder": "Enter your email address for PWPush. (Email & API Key auth)", + "condition": { + "field": "PWPush.UseBearerAuth", + "compareType": "is", + "compareValue": false + } + }, + { + "type": "autoComplete", + "name": "PWPush.AccountId", + "label": "Select your PWPush Account for branding (Pro/Premium only, optional with Custom Domain)", + "multiple": false, + "creatable": false, + "api": { + "url": "/api/ExecExtensionMapping?List=PWPushFields", + "queryKey": "PWPushAccounts", + "dataKey": "Accounts", + "labelField": "name", + "valueField": "id", + "showRefresh": true + }, + "condition": { + "field": "PWPush.UseBearerAuth", + "compareType": "is", + "compareValue": true + } }, { "type": "number", @@ -386,6 +421,11 @@ "label": "Expiration after views", "placeholder": "Expiration after views. (optional)" }, + { + "type": "switch", + "name": "PWPush.RetrievalStep", + "label": "Click to retrieve password (recommended)" + }, { "type": "switch", "name": "PWPush.DeletableByViewer", @@ -426,7 +466,7 @@ }, { "name": "CloudFlare ZTNA Tunnel", - "id": "CTZTNA", + "id": "CFZTNA", "type": "ztna", "cat": "Zero Trust Network Tunnel", "logo": "/assets/integrations/cloudflare.png", @@ -446,20 +486,50 @@ ], "SettingOptions": [ { - "type": "password", - "name": "CloudFlareClientID.APIKey", + "type": "switch", + "name": "CFZTNA.Enabled", + "label": "Enable Integration" + }, + { + "type": "textField", + "name": "CFZTNA.ClientID", "label": "CloudFlare Tunnel Service Account Client ID" }, { "type": "password", - "name": "CloudFlareAPIKey.APIKey", + "name": "CFZTNA.APIKey", "label": "CloudFlare Tunnel Service Account Client Secret" + } + ], + "mappingRequired": false + }, + { + "name": "GitHub", + "id": "GitHub", + "type": "DevOps", + "cat": "Source Control", + "logo": "/assets/integrations/github.png", + "logoDark": "/assets/integrations/github_dark.png", + "description": "Enable the GitHub integration to manage your repositories from CIPP.", + "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositorities functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.", + "links": [ + { + "name": "GitHub Token", + "url": "https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token" + } + ], + "SettingOptions": [ + { + "type": "password", + "name": "GitHub.APIKey", + "label": "GitHub Personal Access Token", + "placeholder": "Enter your GitHub PAT", + "required": true }, { - "_comment": "I have added this switch as a logic check for the Hudu integration script to check against when CIPP first connects to the Hudu Instance via Connect-HuduAPI.ps1", "type": "switch", - "name": "Hudu.CFEnabled", - "label": " Connect to HUDU through CloudFlare Tunnel with the above Service Account credentials." + "name": "GitHub.Enabled", + "label": "Enable Integration" } ], "mappingRequired": false diff --git a/src/data/alerts.json b/src/data/alerts.json index d027afab9869..60d288d7329e 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -81,6 +81,15 @@ "label": "Alert on overused licenses", "recommendedRunInterval": "7d" }, + { + "name": "EntraLicenseUtilization", + "label": "Alert on Entra ID P1/P2 license over-utilization", + "recommendedRunInterval": "7d", + "requiresInput": true, + "inputType": "textField", + "inputLabel": "Alert when utilization exceeds % (default: 110)", + "inputName": "EntraLicenseUtilizationThreshold" + }, { "name": "AppSecretExpiry", "label": "Alert on expiring application secrets", @@ -92,7 +101,7 @@ "recommendedRunInterval": "30d" }, { - "name": "AppCertificateExpiry ", + "name": "AppCertificateExpiry", "label": "Alert on expiring application certificates", "recommendedRunInterval": "1d" }, diff --git a/src/data/portals.json b/src/data/portals.json index 130c9f2d11e4..d5fdbd5ebeb0 100644 --- a/src/data/portals.json +++ b/src/data/portals.json @@ -6,7 +6,7 @@ "variable": "customerId", "target": "_blank", "external": true, - "icon": "cog" + "icon": "GlobeAltIcon" }, { "label": "Exchange Portal", @@ -15,7 +15,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "mail-bulk" + "icon": "Mail" }, { "label": "Entra Portal", @@ -24,7 +24,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "users" + "icon": "UsersIcon" }, { "label": "Teams Portal", @@ -33,7 +33,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "comments" + "icon": "FilePresent" }, { "label": "Azure Portal", @@ -42,7 +42,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "server" + "icon": "ServerIcon" }, { "label": "Intune Portal", @@ -51,7 +51,16 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "laptop-code" + "icon": "Laptop" + }, + { + "label": "SharePoint Admin", + "name": "SharePoint_Admin", + "url": "https://admin.microsoft.com/Partner/beginclientsession.aspx?CTID=customerId&CSDEST=SharePoint", + "variable": "customerId", + "target": "_blank", + "external": true, + "icon": "Share" }, { "label": "Security Portal", @@ -60,7 +69,7 @@ "variable": "customerId", "target": "_blank", "external": true, - "icon": "shield-alt" + "icon": "Shield" }, { "label": "Compliance Portal", @@ -69,15 +78,6 @@ "variable": "customerId", "target": "_blank", "external": true, - "icon": "shield-alt" - }, - { - "label": "SharePoint Admin", - "name": "SharePoint_Admin", - "url": "https://admin.microsoft.com/Partner/beginclientsession.aspx?CTID=customerId&CSDEST=SharePoint", - "variable": "customerId", - "target": "_blank", - "external": true, - "icon": "book" + "icon": "ShieldMoon" } ] diff --git a/src/data/standards.json b/src/data/standards.json index 4a7a5527ae36..b027c365d60b 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2,7 +2,7 @@ { "name": "standards.MailContacts", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information.", "docsDescription": "", "addedComponent": [ @@ -40,7 +40,7 @@ { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": ["lowimpact", "CIS", "mip_search_auditlog"], + "tag": ["CIS", "mip_search_auditlog"], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "addedComponent": [], "label": "Enable the Unified Audit Log", @@ -52,13 +52,14 @@ { "name": "standards.ProfilePhotos", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Controls whether users can set their own profile photos in Microsoft 365.", "docsDescription": "Controls whether users can set their own profile photos in Microsoft 365. When disabled, only User and Global administrators can update profile photos for users.", "addedComponent": [ { "type": "select", "multiple": false, + "creatable": false, "label": "Select value", "name": "standards.ProfilePhotos.state", "options": [ @@ -82,7 +83,7 @@ { "name": "standards.PhishProtection", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Adds branding to the logon page that only appears if the url is not login.microsoftonline.com. This potentially prevents AITM attacks via EvilNginx. This will also automatically generate alerts if a clone of your login page has been found when set to Remediate.", "addedComponent": [], "label": "Enable Phishing Protection system via branding CSS", @@ -99,7 +100,7 @@ { "name": "standards.Branding", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the branding for the tenant. This includes the login page, and the Office 365 portal.", "addedComponent": [ { @@ -155,7 +156,7 @@ { "name": "standards.EnableCustomerLockbox", "cat": "Global Standards", - "tag": ["lowimpact", "CIS", "CustomerLockBoxEnabled"], + "tag": ["CIS", "CustomerLockBoxEnabled"], "helpText": "Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data", "docsDescription": "Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.", "addedComponent": [], @@ -168,7 +169,7 @@ { "name": "standards.EnablePronouns", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables the Pronouns feature for the tenant. This allows users to set their pronouns in their profile.", "addedComponent": [], "label": "Enable Pronouns", @@ -180,7 +181,7 @@ { "name": "standards.AnonReportDisable", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Shows usernames instead of pseudo anonymised names in reports. This standard is required for reporting to work correctly.", "docsDescription": "Microsoft announced some APIs and reports no longer return names, to comply with compliance and legal requirements in specific countries. This proves an issue for a lot of MSPs because those reports are often helpful for engineers. This standard applies a setting that shows usernames in those API calls / reports.", "addedComponent": [], @@ -193,7 +194,7 @@ { "name": "standards.DisableGuestDirectory", "cat": "Global Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.", "docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)", "addedComponent": [], @@ -219,12 +220,13 @@ { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": ["mediumimpact", "CIS", "spo_idle_session_timeout"], + "tag": ["CIS", "spo_idle_session_timeout"], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "addedComponent": [ { "type": "select", "multiple": false, + "creatable": false, "label": "Select value", "name": "standards.ActivityBasedTimeout.timeout", "options": [ @@ -257,10 +259,68 @@ "powershellEquivalent": "Portal or Graph API", "recommendedBy": ["CIS"] }, + { + "name": "standards.AuthMethodsSettings", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.", + "docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "required": false, + "name": "standards.AuthMethodsSettings.ReportSuspiciousActivity", + "label": "Report Suspicious Activity Settings", + "options": [ + { + "label": "Microsoft managed", + "value": "default" + }, + { + "label": "Enabled", + "value": "enabled" + }, + { + "label": "Disabled", + "value": "disabled" + } + ] + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "required": false, + "name": "standards.AuthMethodsSettings.SystemCredential", + "label": "System Credential Preferences", + "options": [ + { + "label": "Microsoft managed", + "value": "default" + }, + { + "label": "Enabled", + "value": "enabled" + }, + { + "label": "Disabled", + "value": "disabled" + } + ] + } + ], + "label": "Configure Authentication Methods Policy Settings", + "impact": "Low Impact", + "impactColour": "info", + "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy", + "recommendedBy": [] + }, { "name": "standards.AppDeploy", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.", "docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.", "addedComponent": [ @@ -279,7 +339,7 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", "addedComponent": [], @@ -292,7 +352,7 @@ { "name": "standards.PWdisplayAppInformationRequiredState", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.", "docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request", "addedComponent": [], @@ -305,7 +365,7 @@ { "name": "standards.allowOTPTokens", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Allows you to use MS authenticator OTP token generator", "docsDescription": "Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients.", "addedComponent": [], @@ -318,13 +378,14 @@ { "name": "standards.PWcompanionAppAllowedState", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication.", "docsDescription": "Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app.", "addedComponent": [ { "type": "select", "multiple": false, + "creatable": false, "label": "Select value", "name": "standards.PWcompanionAppAllowedState.state", "options": [ @@ -348,7 +409,7 @@ { "name": "standards.EnableFIDO2", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables the FIDO2 authenticationMethod for the tenant", "docsDescription": "Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication.", "addedComponent": [], @@ -361,7 +422,7 @@ { "name": "standards.EnableHardwareOAuth", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", "addedComponent": [], @@ -374,7 +435,7 @@ { "name": "standards.allowOAuthTokens", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Allows you to use any software OAuth token generator", "docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.", "addedComponent": [], @@ -387,13 +448,14 @@ { "name": "standards.TAP", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", "addedComponent": [ { "type": "select", "multiple": false, + "creatable": false, "label": "Select TAP Lifetime", "name": "standards.TAP.config", "options": [ @@ -417,7 +479,7 @@ { "name": "standards.PasswordExpireDisabled", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact", "CIS", "PWAgePolicyNew"], + "tag": ["CIS", "PWAgePolicyNew"], "helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.", "docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.", "addedComponent": [], @@ -430,12 +492,13 @@ { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", "addedComponent": [ { "type": "select", "multiple": false, + "creatable": false, "label": "Select value", "name": "standards.ExternalMFATrusted.state", "options": [ @@ -459,7 +522,7 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles. ", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "addedComponent": [], @@ -472,7 +535,7 @@ { "name": "standards.EnableAppConsentRequests", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings", "docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards", "addedComponent": [ @@ -491,13 +554,14 @@ { "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the state of the registration campaign for the tenant", "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", "addedComponent": [ { "type": "select", "multiple": false, + "creatable": false, "label": "Select value", "name": "standards.NudgeMFA.state", "options": [ @@ -527,7 +591,7 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "addedComponent": [], @@ -540,7 +604,7 @@ { "name": "standards.DisableAppCreation", "cat": "Entra (AAD) Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", "addedComponent": [], @@ -608,7 +672,7 @@ { "name": "standards.OauthConsent", "cat": "Entra (AAD) Standards", - "tag": ["mediumimpact", "CIS"], + "tag": ["CIS"], "helpText": "Disables users from being able to consent to applications, except for those specified in the field below", "docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.", "addedComponent": [ @@ -628,7 +692,7 @@ { "name": "standards.OauthConsentLowSec", "cat": "Entra (AAD) Standards", - "tag": ["mediumimpact", "IntegratedApps"], + "tag": ["IntegratedApps"], "helpText": "Sets the default oauth consent level so users can consent to applications that have low risks.", "docsDescription": "Allows users to consent to applications with low assigned risk.", "label": "Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure)", @@ -645,7 +709,9 @@ "addedComponent": [ { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Who can send invites?", "name": "standards.GuestInvite.allowInvitesFrom", "options": [ @@ -677,7 +743,7 @@ { "name": "standards.StaleEntraDevices", "cat": "Entra (AAD) Standards", - "tag": ["highimpact", "CIS"], + "tag": ["CIS"], "helpText": "Cleans up Entra devices that have not connected/signed in for the specified number of days.", "docsDescription": "Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)", "addedComponent": [ @@ -734,7 +800,7 @@ "impact": "High Impact", "impactColour": "danger", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [] + "recommendedBy": ["CIPP"] }, { "name": "standards.DisableVoice", @@ -747,7 +813,7 @@ "impact": "High Impact", "impactColour": "danger", "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", - "recommendedBy": [] + "recommendedBy": ["CIPP"] }, { "name": "standards.DisableEmail", @@ -774,6 +840,19 @@ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", "recommendedBy": [] }, + { + "name": "standards.DisableQRCodePin", + "cat": "Entra (AAD) Standards", + "tag": ["highimpact"], + "helpText": "This blocks users from using QR Code Pin as an MFA method. If a user only has QR Code Pin as a MFA method, they will be unable to log in.", + "docsDescription": "Disables QR Code Pin as an MFA method for the tenant. If a user only has QR Code Pin as a MFA method, they will be unable to sign in.", + "addedComponent": [], + "label": "Disables QR Code Pin as an MFA method", + "impact": "High Impact", + "impactColour": "danger", + "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", + "recommendedBy": [] + }, { "name": "standards.PerUserMFA", "cat": "Entra (AAD) Standards", @@ -789,7 +868,7 @@ { "name": "standards.OutBoundSpamAlert", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Set the Outbound Spam Alert e-mail address", "docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.", "addedComponent": [ @@ -808,7 +887,7 @@ { "name": "standards.MessageExpiration", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the transport message configuration to timeout a message at 12 hours.", "docsDescription": "Expires messages in the transport queue after 12 hours. Makes the NDR for failed messages show up faster for users. Default is 24 hours.", "addedComponent": [], @@ -821,7 +900,7 @@ { "name": "standards.GlobalQuarantineNotifications", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the Global Quarantine Notification Interval to the selected value. Determines how often the quarantine notification is sent to users.", "docsDescription": "Sets the global quarantine notification interval for the tenant. This is the time between the quarantine notification emails are sent out to users. Default is 24 hours.", "addedComponent": [ @@ -855,7 +934,7 @@ { "name": "standards.DisableTNEF", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF.", "docsDescription": "Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF. Cannot be overridden by the user. For more information, see [Microsoft's documentation.](https://learn.microsoft.com/en-us/exchange/mail-flow/content-conversion/tnef-conversion?view=exchserver-2019)", "addedComponent": [], @@ -868,7 +947,7 @@ { "name": "standards.FocusedInbox", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the default Focused Inbox state for the tenant. This can be overridden by the user.", "docsDescription": "Sets the default Focused Inbox state for the tenant. This can be overridden by the user in their Outlook settings. For more information, see [Microsoft's documentation.](https://support.microsoft.com/en-us/office/focused-inbox-for-outlook-f445ad7f-02f4-4294-a82e-71d8964e3978)", "addedComponent": [ @@ -898,7 +977,7 @@ { "name": "standards.CloudMessageRecall", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the Cloud Message Recall state for the tenant. This allows users to recall messages from the cloud.", "docsDescription": "Sets the default state for Cloud Message Recall for the tenant. By default this is enabled. You can read more about the feature [here.](https://techcommunity.microsoft.com/t5/exchange-team-blog/cloud-based-message-recall-in-exchange-online/ba-p/3744714)", "addedComponent": [ @@ -928,7 +1007,7 @@ { "name": "standards.AutoExpandArchive", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables auto-expanding archives for the tenant", "docsDescription": "Enables auto-expanding archives for the tenant. Does not enable archives for users.", "addedComponent": [], @@ -941,7 +1020,7 @@ { "name": "standards.EnableOnlineArchiving", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables the In-Place Online Archive for all UserMailboxes with a valid license.", "addedComponent": [], "label": "Enable Online Archive for all users", @@ -953,7 +1032,7 @@ { "name": "standards.EnableLitigationHold", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Enables litigation hold for all UserMailboxes with a valid license.", "addedComponent": [], "label": "Enable Litigation Hold for all users", @@ -965,7 +1044,7 @@ { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", "addedComponent": [ @@ -995,7 +1074,7 @@ { "name": "standards.EnableMailTips", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS", "exo_mailtipsenabled"], + "tag": ["CIS", "exo_mailtipsenabled"], "helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements", "addedComponent": [ { @@ -1015,7 +1094,7 @@ { "name": "standards.TeamsMeetingsByDefault", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the default state for automatically turning meetings into Teams meetings for the tenant. This can be overridden by the user in Outlook.", "addedComponent": [ { @@ -1044,7 +1123,7 @@ { "name": "standards.DisableViva", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Disables the daily viva reports for all users.", "docsDescription": "", "addedComponent": [], @@ -1057,7 +1136,7 @@ { "name": "standards.RotateDKIM", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "addedComponent": [], "label": "Rotate DKIM keys that are 1024 bit to 2048 bit", @@ -1069,7 +1148,7 @@ { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Enables DKIM for all domains that currently support it", "addedComponent": [], "label": "Enables DKIM for all domains that currently support it", @@ -1081,7 +1160,7 @@ { "name": "standards.EnableMailboxAuditing", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS", "exo_mailboxaudit"], + "tag": ["CIS", "exo_mailboxaudit"], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "addedComponent": [], @@ -1094,7 +1173,7 @@ { "name": "standards.SendReceiveLimitTenant", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the Send and Receive limits for new users. Valid values are 1MB to 150MB", "addedComponent": [ { @@ -1119,7 +1198,7 @@ { "name": "standards.calDefault", "cat": "Exchange Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the default sharing level for the default calendar, for all users", "docsDescription": "Sets the default sharing level for the default calendar for all users in the tenant. You can read about the different sharing levels [here.](https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxfolderpermission?view=exchange-ps#-accessrights)", "disabledFeatures": { @@ -1190,7 +1269,7 @@ { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS", "exo_individualsharing"], + "tag": ["CIS", "exo_individualsharing"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "addedComponent": [], @@ -1200,10 +1279,22 @@ "powershellEquivalent": "Get-SharingPolicy | Set-SharingPolicy -Enabled $False", "recommendedBy": ["CIS"] }, + { + "name": "standardsAutoAddProxy", + "cat": "Exchange Standards", + "tag": ["CIS"], + "helpText": "Automatically adds all available domains as a proxy address.", + "docsDescription": "Automatically finds all available domain names in the tenant, and tries to add proxyaddresses based on the users UPN to each of these.", + "addedComponent": [], + "label": "Automatically deploy proxy addresses", + "impact": "Medium Impact", + "impactColour": "warning", + "powershellEquivalent": "set-mailbox -emailaddresses @{add=$emailaddress}" + }, { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": ["lowimpact", "CIS", "exo_storageproviderrestricted"], + "tag": ["CIS", "exo_storageproviderrestricted"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "addedComponent": [], @@ -1291,7 +1382,7 @@ { "name": "standards.DisableOutlookAddins", "cat": "Exchange Standards", - "tag": ["mediumimpact", "CIS", "exo_outlookaddins"], + "tag": ["CIS", "exo_outlookaddins"], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", "addedComponent": [], @@ -1389,7 +1480,7 @@ { "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", - "tag": ["mediumimpact", "CIS"], + "tag": ["CIS"], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "addedComponent": [], @@ -1402,7 +1493,7 @@ { "name": "standards.EXODisableAutoForwarding", "cat": "Exchange Standards", - "tag": ["highimpact", "CIS", "mdo_autoforwardingmode", "mdo_blockmailforward"], + "tag": ["CIS", "mdo_autoforwardingmode", "mdo_blockmailforward"], "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.", "docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.", "addedComponent": [], @@ -1412,10 +1503,30 @@ "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'Off'", "recommendedBy": ["CIS"] }, + { + "name": "standards.RetentionPolicyTag", + "cat": "Exchange Standards", + "tag": ["highimpact"], + "helpText": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", + "docsDescription": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", + "addedComponent": [ + { + "type": "number", + "name": "standards.RetentionPolicyTag.AgeLimitForRetention", + "label": "Retention Days", + "required": true + } + ], + "label": "Retention Policy, permanently delete items in Deleted Items after X days", + "impact": "High Impact", + "impactColour": "danger", + "powershellEquivalent": "Set-RetentionPolicyTag", + "recommendedBy": [] + }, { "name": "standards.QuarantineRequestAlert", "cat": "Defender Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets a e-mail address to alert when a User requests to release a quarantined message.", "docsDescription": "Sets a e-mail address to alert when a User requests to release a quarantined message. This is useful for monitoring and ensuring that the correct messages are released.", "addedComponent": [ @@ -1434,7 +1545,7 @@ { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", - "tag": ["lowimpact", "CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"], + "tag": ["CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"], "helpText": "This creates a safelink policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ { @@ -1463,7 +1574,6 @@ "name": "standards.AntiPhishPolicy", "cat": "Defender Standards", "tag": [ - "lowimpact", "CIS", "mdo_safeattachments", "mdo_highconfidencespamaction", @@ -1672,7 +1782,6 @@ "name": "standards.SafeAttachmentPolicy", "cat": "Defender Standards", "tag": [ - "lowimpact", "CIS", "mdo_safedocuments", "mdo_commonattachmentsfilter", @@ -1683,8 +1792,8 @@ { "type": "select", "multiple": false, - "label": "Action", - "name": "standards.SafeAttachmentPolicy.Action", + "label": "Safe Attachment Action", + "name": "standards.SafeAttachmentPolicy.SafeAttachmentAction", "options": [ { "label": "Allow", @@ -1741,7 +1850,7 @@ { "name": "standards.AtpPolicyForO365", "cat": "Defender Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.", "addedComponent": [ { @@ -1761,7 +1870,7 @@ { "name": "standards.MalwareFilterPolicy", "cat": "Defender Standards", - "tag": ["lowimpact", "CIS", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware"], + "tag": ["CIS", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware"], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ { @@ -1851,7 +1960,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Spam Action", "name": "standards.SpamFilterPolicy.SpamAction", "options": [ @@ -1867,7 +1978,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Spam Quarantine Tag", "name": "standards.SpamFilterPolicy.SpamQuarantineTag", "options": [ @@ -1887,7 +2000,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "High Confidence Spam Action", "name": "standards.SpamFilterPolicy.HighConfidenceSpamAction", "options": [ @@ -1903,7 +2018,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "High Confidence Spam Quarantine Tag", "name": "standards.SpamFilterPolicy.HighConfidenceSpamQuarantineTag", "options": [ @@ -1923,7 +2040,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Bulk Spam Action", "name": "standards.SpamFilterPolicy.BulkSpamAction", "options": [ @@ -1939,7 +2058,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Bulk Quarantine Tag", "name": "standards.SpamFilterPolicy.BulkQuarantineTag", "options": [ @@ -1959,7 +2080,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Phish Spam Action", "name": "standards.SpamFilterPolicy.PhishSpamAction", "options": [ @@ -1975,7 +2098,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "Phish Quarantine Tag", "name": "standards.SpamFilterPolicy.PhishQuarantineTag", "options": [ @@ -1995,7 +2120,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "label": "High Confidence Phish Quarantine Tag", "name": "standards.SpamFilterPolicy.HighConfidencePhishQuarantineTag", "options": [ @@ -2023,7 +2150,7 @@ { "name": "standards.intuneDeviceRetirementDays", "cat": "Intune Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "A value between 0 and 270 is supported. A value of 0 disables retirement, retired devices are removed from Intune after the specified number of days.", "addedComponent": [ { @@ -2041,7 +2168,7 @@ { "name": "standards.intuneBrandingProfile", "cat": "Intune Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the branding profile for the Intune Company Portal app. This is a tenant wide setting and overrules any settings set on the app level.", "addedComponent": [ { @@ -2113,12 +2240,14 @@ { "name": "standards.IntuneComplianceSettings", "cat": "Intune Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the mark devices with no compliance policy assigned as compliance/non compliant and Compliance status validity period.", "addedComponent": [ { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "name": "standards.IntuneComplianceSettings.secureByDefault", "label": "Mark devices with no compliance policy as", "options": [ @@ -2176,7 +2305,7 @@ { "name": "standards.DeletedUserRentention", "cat": "SharePoint Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.", "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.", "addedComponent": [ @@ -2246,7 +2375,7 @@ { "name": "standards.TenantDefaultTimezone", "cat": "SharePoint Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Sets the default timezone for the tenant. This will be used for all new users and sites.", "addedComponent": [ { @@ -2264,7 +2393,7 @@ { "name": "standards.SPAzureB2B", "cat": "SharePoint Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled", "addedComponent": [], "label": "Enable SharePoint and OneDrive integration with Azure AD B2B", @@ -2276,7 +2405,7 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": ["lowimpact", "CIS"], + "tag": ["CIS"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "addedComponent": [], "label": "Disallow downloading infected files from SharePoint", @@ -2288,7 +2417,7 @@ { "name": "standards.SPDisableLegacyWorkflows", "cat": "SharePoint Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Disables the creation of new SharePoint 2010 and 2013 classic workflows and removes the 'Return to classic SharePoint' link on modern SharePoint list and library pages.", "addedComponent": [], "label": "Disable Legacy Workflows", @@ -2300,7 +2429,7 @@ { "name": "standards.SPDirectSharing", "cat": "SharePoint Standards", - "tag": ["mediumimpact", "CIS"], + "tag": ["CIS"], "helpText": "Ensure default link sharing is set to Direct in SharePoint and OneDrive", "addedComponent": [], "label": "Default sharing to Direct users", @@ -2312,7 +2441,7 @@ { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": ["mediumimpact", "CIS"], + "tag": ["CIS"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "addedComponent": [ { @@ -2330,7 +2459,7 @@ { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": ["mediumimpact", "CIS"], + "tag": ["CIS"], "helpText": "Ensure reauthentication with verification code is restricted", "addedComponent": [ { @@ -2354,6 +2483,7 @@ { "type": "autoComplete", "multiple": false, + "creatable": false, "label": "Add Shortcuts To OneDrive button state", "name": "standards.DisableAddShortcutsToOneDrive.state", "options": [ @@ -2383,6 +2513,7 @@ { "type": "autoComplete", "multiple": false, + "creatable": false, "label": "SharePoint Sync Button state", "name": "standards.SPSyncButtonState.state", "options": [ @@ -2406,7 +2537,7 @@ { "name": "standards.DisableSharePointLegacyAuth", "cat": "SharePoint Standards", - "tag": ["mediumimpact", "CIS", "spo_legacy_auth"], + "tag": ["CIS", "spo_legacy_auth"], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", "addedComponent": [], @@ -2419,7 +2550,7 @@ { "name": "standards.sharingCapability", "cat": "SharePoint Standards", - "tag": ["highimpact", "CIS"], + "tag": ["CIS"], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", "addedComponent": [ { @@ -2456,7 +2587,7 @@ { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": ["highimpact", "CIS"], + "tag": ["CIS"], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "addedComponent": [], @@ -2524,7 +2655,7 @@ { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": ["highimpact", "CIS"], + "tag": ["CIS"], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "addedComponent": [ { @@ -2563,12 +2694,14 @@ { "name": "standards.TeamsGlobalMeetingPolicy", "cat": "Teams Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl", "addedComponent": [ { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "name": "standards.TeamsGlobalMeetingPolicy.DesignatedPresenterRoleMode", "label": "Default value of the `Who can present?`", "options": [ @@ -2597,7 +2730,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "name": "standards.TeamsGlobalMeetingPolicy.MeetingChatEnabledType", "label": "Meeting chat policy", "options": [ @@ -2625,7 +2760,7 @@ { "name": "standards.TeamsEmailIntegration", "cat": "Teams Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Should users be allowed to send emails directly to a channel email addresses?", "docsDescription": "Teams channel email addresses are an optional feature that allows users to email the Teams channel directly.", "addedComponent": [ @@ -2644,7 +2779,7 @@ { "name": "standards.TeamsExternalFileSharing", "cat": "Teams Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.", "addedComponent": [ { @@ -2682,12 +2817,15 @@ { "name": "standards.TeamsEnrollUser", "cat": "Teams Standards", - "tag": ["lowimpact"], + "tag": [], "helpText": "Controls whether users with this policy can set the voice profile capture and enrollment through the Recognition tab in their Teams client settings.", "docsDescription": "Controls whether users with this policy can set the voice profile capture and enrollment through the Recognition tab in their Teams client settings.", "addedComponent": [ { "type": "autoComplete", + "required": true, + "multiple": false, + "creatable": false, "name": "standards.TeamsEnrollUser.EnrollUserOverride", "label": "Voice and Face Enrollment", "options": [ @@ -2756,7 +2894,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "name": "standards.TeamsFederationConfiguration.DomainControl", "label": "Communication Mode", "options": [ @@ -2824,7 +2964,9 @@ }, { "type": "autoComplete", + "required": true, "multiple": false, + "creatable": false, "name": "standards.TeamsMessagingPolicy.ReadReceiptsEnabledType", "label": "Read Receipts Enabled Type", "options": [ @@ -2876,7 +3018,7 @@ { "name": "standards.AutopilotStatusPage", "cat": "Device Management Standards", - "tag": ["lowimpact"], + "tag": [], "disabledFeatures": { "report": true, "warn": true, @@ -2947,7 +3089,7 @@ { "name": "standards.AutopilotProfile", "cat": "Device Management Standards", - "tag": ["lowimpact"], + "tag": [], "disabledFeatures": { "report": true, "warn": true, @@ -2974,6 +3116,7 @@ { "type": "autoComplete", "multiple": false, + "creatable": false, "name": "standards.AutopilotProfile.Languages", "label": "Languages", "api": { @@ -3052,12 +3195,13 @@ "warn": true, "remediate": false }, - "impact": "High", + "impact": "High Impact", "helpText": "Deploy and manage Intune templates across devices.", "addedComponent": [ { "type": "autoComplete", "multiple": false, + "creatable": false, "name": "TemplateList", "label": "Select Intune Template", "api": { @@ -3096,7 +3240,7 @@ "warn": true, "remediate": false }, - "impact": "Medium", + "impact": "Medium Impact", "helpText": "Deploy transport rules to manage email flow.", "addedComponent": [ { @@ -3122,7 +3266,7 @@ "warn": true, "remediate": false }, - "impact": "High", + "impact": "High Impact", "helpText": "Manage conditional access policies for better security.", "addedComponent": [ { @@ -3158,7 +3302,7 @@ "warn": true, "remediate": false }, - "impact": "Medium", + "impact": "Medium Impact", "helpText": "Deploy and manage Exchange connectors.", "addedComponent": [ { @@ -3184,7 +3328,7 @@ "warn": true, "remediate": false }, - "impact": "Medium", + "impact": "Medium Impact", "helpText": "Deploy and manage group templates.", "addedComponent": [ { diff --git a/src/layouts/config.js b/src/layouts/config.js index d9685ba29de4..e7093b9f1883 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -154,15 +154,15 @@ export const nativeMenuItems = [ items: [ { title: "Licence Report", - path: "/tenant/administration/list-licenses", + path: "/tenant/reports/list-licenses", }, { title: "Sherweb Licence Report", - path: "/tenant/administration/list-csp-licenses", + path: "/tenant/reports/list-csp-licenses", }, { title: "Consented Applications", - path: "/tenant/administration/application-consent", + path: "/tenant/reports/application-consent", }, ], }, @@ -244,12 +244,13 @@ export const nativeMenuItems = [ title: "Device Management", path: "/endpoint/MEM", items: [ - { title: "Devices", path: "/endpoint/reports/devices" }, + { title: "Devices", path: "/endpoint/MEM/devices" }, { title: "Configuration Policies", path: "/endpoint/MEM/list-policies" }, { title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" }, { title: "Protection Policies", path: "/endpoint/MEM/list-appprotection-policies" }, { title: "Apply Policy", path: "/endpoint/MEM/add-policy" }, { title: "Policy Templates", path: "/endpoint/MEM/list-templates" }, + { title: "Scripts", path: "/endpoint/MEM/list-scripts" }, ], }, { @@ -300,7 +301,7 @@ export const nativeMenuItems = [ items: [ { title: "Administration", - path: "/email/Administration", + path: "/email/administration", items: [ { title: "Mailboxes", path: "/email/administration/mailboxes" }, { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" }, @@ -315,17 +316,17 @@ export const nativeMenuItems = [ }, { title: "Transport", - path: "/email/Transport", + path: "/email/transport", items: [ { title: "Transport rules", path: "/email/transport/list-rules" }, { title: "Transport Templates", path: "/email/transport/list-templates", }, - { title: "Connectors", path: "/email/connectors/list-connectors" }, + { title: "Connectors", path: "/email/transport/list-connectors" }, { title: "Connector Templates", - path: "/email/connectors/list-connector-templates", + path: "/email/transport/list-connector-templates", }, ], }, @@ -335,8 +336,11 @@ export const nativeMenuItems = [ items: [ { title: "Spamfilter", path: "/email/spamfilter/list-spamfilter" }, { title: "Spamfilter templates", path: "/email/spamfilter/list-templates" }, - { title: "Connection filter", path: "/email/connectionfilter/list-connectionfilter" }, - { title: "Connection filter templates", path: "/email/connectionfilter/list-templates" }, + { title: "Connection filter", path: "/email/spamfilter/list-connectionfilter" }, + { + title: "Connection filter templates", + path: "/email/spamfilter/list-connectionfilter-templates", + }, ], }, { @@ -399,11 +403,11 @@ export const nativeMenuItems = [ items: [ { title: "Graph Explorer", - path: "/tenant/administration/graph-explorer", + path: "/tenant/tools/graph-explorer", }, { title: "Application Approval", - path: "/tenant/administration/appapproval", + path: "/tenant/tools/appapproval", }, { title: "Tenant Lookup", path: "/tenant/tools/tenantlookup" }, @@ -411,7 +415,7 @@ export const nativeMenuItems = [ { title: "Individual Domain Check", - path: "/tenant/standards/individual-domains", + path: "/tenant/tools/individual-domains", }, ], }, @@ -437,6 +441,11 @@ export const nativeMenuItems = [ path: "/tools/templatelib", roles: ["editor", "admin", "superadmin"], }, + { + title: "Community Repositories", + path: "/tools/community-repos", + roles: ["editor", "admin", "superadmin"], + }, { title: "Scheduler", path: "/cipp/scheduler", diff --git a/src/layouts/index.js b/src/layouts/index.js index 9538706081fa..33ffc87aa2eb 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -84,11 +84,14 @@ export const Layout = (props) => { url: "/.auth/me", queryKey: "authmecipp", }); + const [hideSidebar, setHideSidebar] = useState(false); useEffect(() => { if (currentRole.isSuccess && !currentRole.isFetching) { const userRoles = currentRole.data?.clientPrincipal?.userRoles; if (!userRoles) { + setMenuItems([]); + setHideSidebar(true); return; } const filterItemsByRole = (items) => { @@ -200,15 +203,19 @@ export const Layout = (props) => { return ( <> - - {mdDown && ( - + {hideSidebar === false && ( + <> + + {mdDown && ( + + )} + {!mdDown && } + )} - {!mdDown && } diff --git a/src/pages/_app.js b/src/pages/_app.js index 702eb328927c..e520d50c5822 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -20,6 +20,14 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import TimeAgo from "javascript-time-ago"; import en from "javascript-time-ago/locale/en.json"; +import CippSpeedDial from "../components/CippComponents/CippSpeedDial"; +import { + Help as HelpIcon, + BugReport as BugReportIcon, + Feedback as FeedbackIcon, +} from "@mui/icons-material"; +import { SvgIcon } from "@mui/material"; +import discordIcon from "../../public/discord-mark-blue.svg"; import React from "react"; TimeAgo.addDefaultLocale(en); @@ -36,6 +44,33 @@ const App = (props) => { const getLayout = Component.getLayout ?? ((page) => page); const preferredTheme = useMediaPredicate("(prefers-color-scheme: dark)") ? "dark" : "light"; + const speedDialActions = [ + { + id: "bug-report", + icon: , + name: "Report Bug", + href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml", + onClick: () => window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml", "_blank") + }, + { + id: "feature-request", + icon: , + name: "Request Feature", + href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml", + onClick: () => window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml", "_blank") + }, + { + id: "discord", + icon: ( + + + ), + name: "Join the Discord!", + href: "https://discord.gg/cyberdrain", + onClick: () => window.open("https://discord.gg/cyberdrain", "_blank") + }, + ]; + return ( @@ -69,6 +104,11 @@ const App = (props) => { {getLayout()} + } + position={{ bottom: 16, right: 16 }} + /> {settings?.showDevtools && ( diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js index 849bed69bb43..d6cb9caf1a2b 100644 --- a/src/pages/cipp/advanced/table-maintenance.js +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -274,7 +274,7 @@ const Page = () => { }; return ( - + {pageTitle} @@ -282,7 +282,7 @@ const Page = () => { This page allows you to view and manage data in Azure Tables. This is advanced functionality that should only be used when directed by CyberDrain support. - + { defaultValues: integrations?.data, }); - const extension = extensions.find((extension) => extension.id === router.query.id); + const extension = extensions.find((extension) => extension.id === router.query.id) || {}; var logo = extension?.logo; if (preferredTheme === "dark" && extension?.logoDark) { @@ -95,13 +106,23 @@ const Page = () => { return ( <> + {integrations.isLoading && ( + + + + + + + + + + )} {integrations.isSuccess && extension && ( {logo && ( @@ -109,7 +130,7 @@ const Page = () => { component="img" src={logo} alt={extension.name} - sx={{ width: "50%", mx: "auto" }} + sx={{ maxWidth: "50%", mx: "auto", maxHeight: "125px" }} /> )} @@ -120,54 +141,64 @@ const Page = () => { {extension.alertText} )} - + {extension?.hideTestButton !== true && ( - + + + )} {extension?.forceSyncButton && ( - + + + )} {extension?.links && ( <> {extension.links.map((link, index) => ( - + + + ))} )} - - + + @@ -179,8 +210,13 @@ const Page = () => { - + {extension?.id === "cippapi" ? ( + + ) : ( + + )} + {extension?.mappingRequired && ( diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index 241ced5c4792..4bf582e23432 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -56,11 +56,11 @@ const Page = () => { } var integrationConfig = integrations?.data?.[extension.id]; - var isEnabled = integrationConfig?.Enabled; + var isEnabled = integrationConfig?.Enabled || extension.id === "cippapi"; var status = "Unconfigured"; if (integrationConfig && !isEnabled) { status = "Disabled"; - } else if (integrationConfig && isEnabled) { + } else if ((integrationConfig && isEnabled) || extension.id === "cippapi") { status = "Enabled"; } diff --git a/src/pages/cipp/logs/index.js b/src/pages/cipp/logs/index.js index f98368546531..cb6b39500aa4 100644 --- a/src/pages/cipp/logs/index.js +++ b/src/pages/cipp/logs/index.js @@ -21,6 +21,8 @@ const simpleColumns = [ "Message", "API", "Severity", + "AppId", + "IP", "LogData", ]; @@ -95,6 +97,7 @@ const Page = () => { apiUrl={apiUrl} simpleColumns={simpleColumns} queryKey={`Listlogs-${dateFilter}-${filterEnabled}`} + tenantInTitle={false} apiData={{ DateFilter: dateFilter, // Pass date filter from state Filter: filterEnabled, // Pass filter toggle state diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js index 6cc782d44508..a6c34d96bbab 100644 --- a/src/pages/cipp/preferences.js +++ b/src/pages/cipp/preferences.js @@ -255,6 +255,26 @@ const Page = () => { /> ), }, + { + label: "Disable Sign in", + value: ( + + ), + }, + { + label: "Remove all MFA Devices", + value: ( + + ), + }, ]} /> diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 18b0d01a89b5..97abcc20b602 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -34,6 +34,29 @@ const Page = () => { }, ]; + const filterList = [ + { + filterName: "Running", + value: [{ id: "TaskState", value: "Running" }], + type: "column", + }, + { + filterName: "Planned", + value: [{ id: "TaskState", value: "Planned" }], + type: "column", + }, + { + filterName: "Failed", + value: [{ id: "TaskState", value: "Failed" }], + type: "column", + }, + { + filterName: "Completed", + value: [{ id: "TaskState", value: "Completed" }], + type: "column", + }, + ]; + const offCanvas = { children: (extendedData) => ( <> @@ -75,6 +98,7 @@ const Page = () => { ]} actions={actions} offCanvas={offCanvas} + filters={filterList} /> ); }; diff --git a/src/pages/cipp/settings/notifications.js b/src/pages/cipp/settings/notifications.js index 2dd5a990d5e0..b9fc1cd25aa6 100644 --- a/src/pages/cipp/settings/notifications.js +++ b/src/pages/cipp/settings/notifications.js @@ -81,6 +81,7 @@ const Page = () => { resetForm={false} postUrl="/api/ExecNotificationConfig" relatedQueryKeys={["ListNotificationConfig"]} + isFetching={listNotificationConfig.isFetching} addedButtons={ diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index 81726a1abd14..90bfc401266e 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -2,6 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import Link from "next/link"; import { Button } from "@mui/material"; +import { Add } from "@mui/icons-material"; import { Archive, @@ -131,6 +132,93 @@ const Page = () => { confirmText: "Are you sure you want to delete this mailbox?", multiPost: false, }, + { + label: "Copy Sent Items to Shared Mailbox", + type: "GET", + url: "/api/ExecCopyForSent", + data: { ID: "UPN" }, + confirmText: "Are you sure you want to enable Copy Sent Items to Shared Mailbox?", + icon: , + condition: (row) => + row.MessageCopyForSentAsEnabled === false && row.recipientTypeDetails === "SharedMailbox", + }, + { + label: "Disable Copy Sent Items to Shared Mailbox", + type: "GET", + url: "/api/ExecCopyForSent", + data: { ID: "UPN", MessageCopyForSentAsEnabled: false }, + confirmText: "Are you sure you want to disable Copy Sent Items to Shared Mailbox?", + icon: , + condition: (row) => + row.MessageCopyForSentAsEnabled === true && row.recipientTypeDetails === "SharedMailbox", + }, + { + label: "Set mailbox locale", + type: "POST", + url: "/api/ExecSetMailboxLocale", + data: { user: "UPN", ProhibitSendQuota: true }, + confirmText: "Enter a locale, e.g. en-US", + icon: , + fields: [ + { + label: "Locale", + name: "locale", + type: "textField", + placeholder: "e.g. en-US", + }, + ], + }, + { + label: "Set Send Quota", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { user: "UPN", ProhibitSendQuota: true }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + }, + ], + }, + { + label: "Set Send and Receive Quota", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { + user: "UPN", + ProhibitSendReceiveQuota: true, + }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + }, + ], + }, + { + label: "Set Quota Warning Level", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { user: "UPN", IssueWarningQuota: true }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + }, + ], + }, ]; // Define off-canvas details @@ -182,7 +270,11 @@ const Page = () => { filters={filterList} cardButton={ <> - diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js index e784d8888a0e..7415aa9dc5d0 100644 --- a/src/pages/email/administration/quarantine/index.js +++ b/src/pages/email/administration/quarantine/index.js @@ -101,6 +101,7 @@ const Page = () => { }, confirmText: "Are you sure you want to release this message?", icon: , + condition: (row) => row.ReleaseStatus !== "RELEASED", }, { label: "Deny", @@ -112,6 +113,7 @@ const Page = () => { }, confirmText: "Are you sure you want to deny this message?", icon: , + condition: (row) => row.ReleaseStatus !== "DENIED", }, { label: "Release & Allow Sender", @@ -125,6 +127,7 @@ const Page = () => { confirmText: "Are you sure you want to release this email and add the sender to the whitelist?", icon: , + condition: (row) => row.ReleaseStatus !== "RELEASED", }, ]; diff --git a/src/pages/email/administration/tenant-allow-block-lists/index.js b/src/pages/email/administration/tenant-allow-block-lists/index.js index 97925a29f708..95f80532ee5c 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/index.js +++ b/src/pages/email/administration/tenant-allow-block-lists/index.js @@ -2,6 +2,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import TrashIcon from "@heroicons/react/24/outline/TrashIcon"; +import { PlaylistAdd } from "@mui/icons-material"; const Page = () => { const pageTitle = "Tenant Allow/Block Lists"; @@ -12,12 +14,12 @@ const Page = () => { type: "POST", url: "/api/RemoveTenantAllowBlockList", data: { - TenantFilter: "Tenant", Entries: "Value", ListType: "ListType", }, confirmText: "Are you sure you want to delete?", color: "danger", + icon: , }, ]; @@ -48,7 +50,11 @@ const Page = () => { }} cardButton={ <> - diff --git a/src/pages/email/connectionfilter/deploy/index.js b/src/pages/email/connectionfilter/deploy/index.js deleted file mode 100644 index 92cbabc77ae2..000000000000 --- a/src/pages/email/connectionfilter/deploy/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Apply Spamfilter Template"; - - return ( -
-

{pageTitle}

-

This is a placeholder page for the apply spamfilter template section.

-
- ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/connectionfilter/list-templates/index.js b/src/pages/email/connectionfilter/list-templates/index.js deleted file mode 100644 index 4a104db53a18..000000000000 --- a/src/pages/email/connectionfilter/list-templates/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; - -const Page = () => { - const pageTitle = "Connection filter Templates"; - - const actions = [ - { - label: "Delete Template", - type: "POST", - url: "/api/RemoveConnectionfilterTemplate", - data: { ID: "GUID" }, - confirmText: "Do you want to delete the template?", - icon: , - color: "danger", - }, - ]; - - const offCanvas = { - extendedInfoFields: [ - "name", - "IsDefault", - "IPAllowList", - "IPBlockList", - "EnableSafeList", - "GUID", - ], - actions: actions, - }; - - const simpleColumns = [ - "name", - "IsDefault", - "IPAllowList", - "IPBlockList", - "EnableSafeList", - "GUID", - ]; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; -export default Page; diff --git a/src/pages/email/connectors/deploy-connector/index.js b/src/pages/email/connectors/deploy-connector/index.js deleted file mode 100644 index cddfb2691027..000000000000 --- a/src/pages/email/connectors/deploy-connector/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Deploy Connector Templates"; - - return ( -
-

{pageTitle}

-

This is a placeholder page for the deploy connector templates section.

-
- ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/connectors/list-connector-templates/index.js b/src/pages/email/connectors/list-connector-templates/index.js deleted file mode 100644 index 671c148f21e2..000000000000 --- a/src/pages/email/connectors/list-connector-templates/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; -import ConnectorTemplateDetails from "../../../../components/CippComponents/ConnectorTemplateDetails"; - -const Page = () => { - const pageTitle = "Exchange Connector Templates"; - - const actions = [ - { - label: "Delete Template", - type: "POST", - url: "/api/RemoveExConnectorTemplate", - data: { - ID: "GUID", - }, - confirmText: "Do you want to delete the template?", - icon: , - color: "danger", - }, - ]; - - const offCanvas = { - children: (data) => , - actions: actions, - size: "lg", - }; - - const simpleColumns = ["name", "cippconnectortype", "GUID"]; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; -export default Page; diff --git a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js index c72597ef6a25..186556bbe91e 100644 --- a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js +++ b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block } from "@mui/icons-material"; /* NOTE for Devs: @@ -18,10 +19,11 @@ const Page = () => { actions={[ { label: "Block Sign In", - type: "POST", + type: "GET", + icon: , url: "/api/ExecDisableUser", - data: { TenantFilter: "Tenant", ID: "id" }, - confirmText: "Are you sure you want to block this user from signing in?", + data: { ID: "id" }, + confirmText: "Are you sure you want to block the sign-in for this user?", }, ]} offCanvas={{ diff --git a/src/pages/email/reports/antiphishing-filters/index.js b/src/pages/email/reports/antiphishing-filters/index.js index 96a1080d9498..0d3d3d26e736 100644 --- a/src/pages/email/reports/antiphishing-filters/index.js +++ b/src/pages/email/reports/antiphishing-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; const Page = () => { const pageTitle = "List of Anti-Phishing Filters"; @@ -8,25 +9,27 @@ const Page = () => { const actions = [ { label: "Enable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditAntiPhishingFilter", data: { State: "Enable", - TenantFilter: "TenantFilter", // TenantFilter used in API path as per original file - RuleName: "id", + RuleName: "RuleName", }, confirmText: "Are you sure you want to enable this rule?", + condition: (row) => row.State === "Disabled", }, { label: "Disable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditAntiPhishingFilter", data: { State: "Disable", - TenantFilter: "TenantFilter", - RuleName: "id", + RuleName: "RuleName", }, confirmText: "Are you sure you want to disable this rule?", + condition: (row) => row.State === "Enabled", }, // Uncomment the following block if Delete Rule is to be re-enabled in the future /* @@ -35,8 +38,7 @@ const Page = () => { type: "POST", url: "/api/RemoveAntiPhishingFilter", data: { - TenantFilter: "TenantFilter", - RuleName: "id", + RuleName: "RuleName", }, confirmText: "Are you sure you want to delete this rule?", }, diff --git a/src/pages/email/reports/global-address-list/index.js b/src/pages/email/reports/global-address-list/index.js index 5b0be1040072..314fb23f66f0 100644 --- a/src/pages/email/reports/global-address-list/index.js +++ b/src/pages/email/reports/global-address-list/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; const Page = () => { const actions = [ @@ -7,21 +8,25 @@ const Page = () => { label: "Unhide from Global Address List", type: "POST", url: "/api/ExecHideFromGAL", + icon: , data: { HideFromGAL: false, ID: "PrimarySmtpAddress", }, confirmText: "Are you sure you want to show this mailbox in the Global Address List?", + condition: (row) => row.HiddenFromAddressListsEnabled == true, }, { label: "Hide from Global Address List", type: "POST", url: "/api/ExecHideFromGAL", + icon: , data: { HideFromGAL: true, ID: "PrimarySmtpAddress", }, confirmText: "Are you sure you want to hide this mailbox from the Global Address List?", + condition: (row) => row.HiddenFromAddressListsEnabled == false, }, ]; diff --git a/src/pages/email/reports/malware-filters/index.js b/src/pages/email/reports/malware-filters/index.js index 4d5ffe472d98..5ede0778ed93 100644 --- a/src/pages/email/reports/malware-filters/index.js +++ b/src/pages/email/reports/malware-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; const Page = () => { return ( @@ -9,25 +10,27 @@ const Page = () => { actions={[ { label: "Enable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditMalwareFilter", data: { State: "Enable", - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to enable this rule?", + condition: (row) => row.State === "Disabled", }, { label: "Disable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditMalwareFilter", data: { State: "Disable", - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to disable this rule?", + condition: (row) => row.State === "Enabled", }, /* Uncomment and add additional actions if required by future specs { @@ -35,8 +38,7 @@ const Page = () => { type: "POST", url: "/api/RemoveMalwareFilter", data: { - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to delete this rule?", }, diff --git a/src/pages/email/reports/safeattachments-filters/index.js b/src/pages/email/reports/safeattachments-filters/index.js index b61ac47207bc..11e15a1bdea7 100644 --- a/src/pages/email/reports/safeattachments-filters/index.js +++ b/src/pages/email/reports/safeattachments-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; const Page = () => { const pageTitle = "List of Safe Attachment Filters"; @@ -9,37 +10,38 @@ const Page = () => { const actions = [ { label: "Enable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditSafeAttachmentsFilter", data: { - State: "Enable", - TenantFilter: "tenant.defaultDomainName", // TenantFilter uses default domain name in context - RuleName: "row.RuleName", + State: "!enable", + RuleName: "RuleName", }, confirmText: "Are you sure you want to enable this rule?", color: "info", + condition: (row) => row.State === "Disabled" }, { label: "Disable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditSafeAttachmentsFilter", data: { State: "Disable", - TenantFilter: "tenant.defaultDomainName", // TenantFilter uses default domain name in context - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to disable this rule?", color: "info", + condition: (row) => row.State === "Enabled", }, // Commented out "Delete Rule" action from the original file as it was also commented in legacy code /* { label: "Delete Rule", - type: "POST", + type: "GET", url: "/api/RemoveSafeAttachmentsFilter", data: { - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to delete this rule?", color: "danger", diff --git a/src/pages/email/reports/safelinks-filters/index.js b/src/pages/email/reports/safelinks-filters/index.js index 7d2bf1ad5a37..bee1985d9745 100644 --- a/src/pages/email/reports/safelinks-filters/index.js +++ b/src/pages/email/reports/safelinks-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; /* Note: Tenant information is passed directly in apiData instead of using Redux (e.g., useSelector) */ /* Original file included a "Delete Rule" action. If needed, add back by following other action formats. */ @@ -7,74 +8,82 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" /* Removed custom formatters and FontAwesome imports, as table formatting is handled by CippTablePage */ const Page = () => { + const pageTitle = "List of Safe Link Filters"; + const apiUrl = "/api/ListSafeLinksFilters"; + + const actions = [ + { + label: "Enable Rule", + type: "GET", + icon: , + url: "/api/EditSafeLinksFilter", + data: { + State: "Enable", + RuleName: "RuleName", + }, + confirmText: "Are you sure you want to enable this rule?", + color: "info", + condition: (row) => row.State === "Disabled", + }, + { + label: "Disable Rule", + type: "GET", + icon: , + url: "/api/EditSafeLinksFilter", + data: { + State: "Disable", + RuleName: "RuleName", + }, + confirmText: "Are you sure you want to disable this rule?", + color: "info", + condition: (row) => row.State === "Enabled", + }, + /* TODO: implement Delete Rule action + { + label: "Delete Rule", + type: "GET", + url: "/api/EditSafeLinksFilter", + data: { + RuleName: "RuleName", + }, + confirmText: "Are you sure you want to delete this rule?", + color: "danger", + }, + */ + ]; + + const offCanvas = { + extendedInfoFields: ["RuleName", "Name", "State", "WhenCreated", "WhenChanged"], + actions: actions, // Attaching actions to offCanvas per original design + }; + + const simpleColumns = [ + "RuleName", + "Name", + "State", + "Priority", + "RecipientDomainIs", + "EnableSafeLinksForEmail", + "EnableSafeLinksForTeams", + "EnableSafeLinksForOffice", + "TrackClicks", + "ScanUrls", + "EnableForInternalSenders", + "DeliverMessageAfterScan", + "AllowClickThrough", + "DisableUrlRewrite", + "EnableOrganizationBranding", + "WhenCreated", + "WhenChanged", + ]; + return ( ); }; diff --git a/src/pages/email/resources/management/list-rooms/edit.jsx b/src/pages/email/resources/management/list-rooms/edit.jsx new file mode 100644 index 000000000000..91e9ecd66b11 --- /dev/null +++ b/src/pages/email/resources/management/list-rooms/edit.jsx @@ -0,0 +1,311 @@ +import React, { useEffect } from "react"; +import { Grid, Divider, Typography } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; +import countryList from "/src/data/countryList.json"; + +const EditRoomMailbox = () => { + const router = useRouter(); + const { roomId } = router.query; + const tenantDomain = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + }); + + const roomInfo = ApiGetCall({ + url: `/api/ListRooms?roomId=${roomId}&tenantFilter=${tenantDomain}`, + queryKey: `Room-${roomId}`, + waiting: false, + }); + + useEffect(() => { + if (roomInfo.isSuccess && roomInfo.data?.[0]) { + const room = roomInfo.data[0]; + formControl.reset({ + // Core Properties + displayName: room.displayName, + hiddenFromAddressListsEnabled: room.hiddenFromAddressListsEnabled, + + // Room Booking Settings + capacity: room.capacity, + + // Location Information + building: room.building, + floor: room.floor, + floorLabel: room.floorLabel, + street: room.street, + city: room.city, + state: room.state, + postalCode: room.postalCode, + countryOrRegion: room.countryOrRegion + ? countryList.find((c) => c.Name === room.countryOrRegion)?.Code || "" + : "", + + // Room Equipment + audioDeviceName: room.audioDeviceName, + videoDeviceName: room.videoDeviceName, + displayDeviceName: room.displayDeviceName, + + // Room Features + isWheelChairAccessible: room.isWheelChairAccessible, + phone: room.phone, + tags: room.tags?.map(tag => ({ label: tag, value: tag })) || [], + }); + } + }, [roomInfo.isSuccess, roomInfo.data]); + + useEffect(() => { + if (roomId) { + roomInfo.refetch(); + } + }, [router.query, roomId, tenantDomain]); + + return ( + ({ + tenantID: tenantDomain, + roomId: roomId, + displayName: values.displayName?.trim(), + hiddenFromAddressListsEnabled: values.hiddenFromAddressListsEnabled, + + // Room Booking Settings + capacity: values.capacity, + + // Location Information + building: values.building?.trim(), + floor: values.floor, + floorLabel: values.floorLabel?.trim(), + street: values.street?.trim(), + city: values.city?.trim(), + state: values.state?.trim(), + postalCode: values.postalCode?.trim(), + countryOrRegion: values.countryOrRegion?.value || values.countryOrRegion, + + // Room Equipment + audioDeviceName: values.audioDeviceName?.trim(), + videoDeviceName: values.videoDeviceName?.trim(), + displayDeviceName: values.displayDeviceName?.trim(), + + // Room Features + isWheelChairAccessible: values.isWheelChairAccessible, + phone: values.phone?.trim(), + tags: values.tags?.map(tag => tag.value), + })} + > + + {/* Core & Booking Settings */} + + + + + + + + + + + + + + + + + + + {/* Location Information */} + + Location Information + + + {/* Building and Floor Info */} + + + + + + + + + + + + + + + + {/* Address Fields */} + + + + + + {/* City and Postal Code */} + + + + + + + + + {/* State and Country */} + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + + + + + {/* Room Equipment */} + + Room Equipment + + + + + + + + + + + + + + + + + {/* Room Features */} + + Room Features + + + + + + + + + + + + ); +}; + +EditRoomMailbox.getLayout = (page) => {page}; + +export default EditRoomMailbox; \ No newline at end of file diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js index e81e9313ef7a..9fc307d60e63 100644 --- a/src/pages/email/resources/management/list-rooms/index.js +++ b/src/pages/email/resources/management/list-rooms/index.js @@ -2,17 +2,74 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import { AddHomeWork, Edit, Block, LockOpen } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Rooms"; + const actions = [ + { + label: "Edit Room", + link: `/email/resources/management/list-rooms/edit?roomId=[id]`, + icon: , + color: "info", + condition: (row) => !row.isDirSynced, + }, + { + label: "Block Sign In", + type: "GET", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "id" }, + confirmText: "Are you sure you want to block the sign-in for this room mailbox?", + multiPost: false, + condition: (row) => !row.accountDisabled && !row.isDirSynced, + }, + { + label: "Unblock Sign In", + type: "GET", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "id", Enable: true }, + confirmText: "Are you sure you want to unblock sign-in for this room mailbox?", + multiPost: false, + condition: (row) => row.accountDisabled && !row.isDirSynced, + }, + { + label: "Delete Room", + type: "GET", + icon: , + url: "/api/RemoveMailbox", + data: { ID: "mail" }, + confirmText: "Are you sure you want to delete this room mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + ]; + return ( + } diff --git a/src/pages/email/resources/management/room-lists/index.js b/src/pages/email/resources/management/room-lists/index.js index 11abfc1683cb..e73dcc50490e 100644 --- a/src/pages/email/resources/management/room-lists/index.js +++ b/src/pages/email/resources/management/room-lists/index.js @@ -1,15 +1,17 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Visibility } from "@mui/icons-material"; const Page = () => { const pageTitle = "Room Lists"; - const apiUrl = "/api/ListRoomLists" + const apiUrl = "/api/ListRoomLists"; const actions = [ { label: "View included Rooms", link: `/email/resources/management/room-lists/list/view?roomAddress=[emailAddress]`, color: "info", + icon: , }, ]; @@ -40,6 +42,7 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} actions={actions} + apiDataKey="ListRoomListsResults" offCanvas={offCanvas} simpleColumns={simpleColumns} /> diff --git a/src/pages/email/spamfilter/deploy/index.js b/src/pages/email/spamfilter/deploy/index.js deleted file mode 100644 index 92cbabc77ae2..000000000000 --- a/src/pages/email/spamfilter/deploy/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Apply Spamfilter Template"; - - return ( -
-

{pageTitle}

-

This is a placeholder page for the apply spamfilter template section.

-
- ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/spamfilter/list-connectionfilter-templates/index.js b/src/pages/email/spamfilter/list-connectionfilter-templates/index.js new file mode 100644 index 000000000000..ca0ced2e75ea --- /dev/null +++ b/src/pages/email/spamfilter/list-connectionfilter-templates/index.js @@ -0,0 +1,102 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Connection filter Templates"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveConnectionfilterTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "name", + "IsDefault", + "IPAllowList", + "IPBlockList", + "EnableSafeList", + "GUID", + ], + actions: actions, + }; + + const simpleColumns = [ + "name", + "IsDefault", + "IPAllowList", + "IPBlockList", + "EnableSafeList", + "GUID", + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/email/connectionfilter/list-connectionfilter/add.jsx b/src/pages/email/spamfilter/list-connectionfilter/add.jsx similarity index 100% rename from src/pages/email/connectionfilter/list-connectionfilter/add.jsx rename to src/pages/email/spamfilter/list-connectionfilter/add.jsx diff --git a/src/pages/email/connectionfilter/list-connectionfilter/index.js b/src/pages/email/spamfilter/list-connectionfilter/index.js similarity index 74% rename from src/pages/email/connectionfilter/list-connectionfilter/index.js rename to src/pages/email/spamfilter/list-connectionfilter/index.js index cd6e9482ada8..0ebf8fc05c5b 100644 --- a/src/pages/email/connectionfilter/list-connectionfilter/index.js +++ b/src/pages/email/spamfilter/list-connectionfilter/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Book, AddModerator } from "@mui/icons-material"; import Link from "next/link"; const Page = () => { @@ -8,13 +9,14 @@ const Page = () => { const actions = [ { - label: "Create template based on rule", + label: "Create template based on filter", type: "POST", url: "/api/AddConnectionfilterTemplate", dataFunction: (data) => { return { ...data }; }, - confirmText: "Are you sure you want to create a template based on this rule?", + icon: , + confirmText: "Are you sure you want to create a template based on this filter?", }, ]; @@ -32,13 +34,7 @@ const Page = () => { actions: actions, }; - const simpleColumns = [ - "Name", - "IsDefault", - "IPAllowList", - "IPBlockList", - "EnableSafeList", - ]; + const simpleColumns = ["Name", "IsDefault", "IPAllowList", "IPBlockList", "EnableSafeList"]; return ( { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/email/spamfilter/list-spamfilter/index.js b/src/pages/email/spamfilter/list-spamfilter/index.js index 062f9e71dc87..55985f5d7316 100644 --- a/src/pages/email/spamfilter/list-spamfilter/index.js +++ b/src/pages/email/spamfilter/list-spamfilter/index.js @@ -1,15 +1,20 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Book, Block, Check } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; +import { RocketLaunch } from "@mui/icons-material"; const Page = () => { const pageTitle = "Spam Filters"; + const apiUrl = "/api/ListSpamfilter" const actions = [ { label: "Create template based on rule", type: "POST", + icon: , url: "/api/AddSpamfilterTemplate", dataFunction: (data) => { return { ...data }; @@ -18,32 +23,34 @@ const Page = () => { }, { label: "Enable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditSpamfilter", data: { State: "enable", - TenantFilter: "Tenant", name: "Name", }, confirmText: "Are you sure you want to enable this rule?", + condition: (row) => row.ruleState === "Disabled", }, { label: "Disable Rule", - type: "POST", + type: "GET", + icon: , url: "/api/EditSpamfilter", data: { State: "disable", - TenantFilter: "Tenant", name: "Name", }, confirmText: "Are you sure you want to disable this rule?", + condition: (row) => row.ruleState === "Enabled", }, { label: "Delete Rule", - type: "POST", + type: "GET", + icon: , url: "/api/RemoveSpamFilter", data: { - TenantFilter: "Tenant", name: "Name", }, confirmText: "Are you sure you want to delete this rule?", @@ -88,13 +95,17 @@ const Page = () => { return ( - diff --git a/src/pages/email/spamfilter/list-templates/index.js b/src/pages/email/spamfilter/list-templates/index.js index faa7f9a905a0..ad1e87195129 100644 --- a/src/pages/email/spamfilter/list-templates/index.js +++ b/src/pages/email/spamfilter/list-templates/index.js @@ -1,11 +1,60 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Spamfilter Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "POST", diff --git a/src/pages/email/tools/mailbox-restores/index.js b/src/pages/email/tools/mailbox-restores/index.js index ffa463800bd5..b63d572a32f3 100644 --- a/src/pages/email/tools/mailbox-restores/index.js +++ b/src/pages/email/tools/mailbox-restores/index.js @@ -2,6 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import { RestoreFromTrash, PlayArrow, Pause, Delete } from "@mui/icons-material"; import MailboxRestoreDetails from "../../../../components/CippComponents/MailboxRestoreDetails"; const Page = () => { @@ -12,10 +13,10 @@ const Page = () => { label: "Resume Restore Request", type: "POST", url: "/api/ExecMailboxRestore", + icon: , data: { - TenantFilter: "Tenant", Identity: "Identity", - Action: "Resume", + Action: "!Resume", }, confirmText: "Are you sure you want to resume this restore request?", color: "info", @@ -24,10 +25,10 @@ const Page = () => { label: "Suspend Restore Request", type: "POST", url: "/api/ExecMailboxRestore", + icon: , data: { - TenantFilter: "Tenant", Identity: "Identity", - Action: "Suspend", + Action: "!Suspend", }, confirmText: "Are you sure you want to suspend this restore request?", color: "warning", @@ -36,10 +37,10 @@ const Page = () => { label: "Remove Restore Request", type: "POST", url: "/api/ExecMailboxRestore", + icon: , data: { - TenantFilter: "Tenant", Identity: "Identity", - Action: "Remove", + Action: "!Remove", }, confirmText: "Are you sure you want to remove this restore request?", color: "danger", @@ -63,7 +64,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/email/transport/list-connector-templates/index.js b/src/pages/email/transport/list-connector-templates/index.js new file mode 100644 index 000000000000..7f35e6629322 --- /dev/null +++ b/src/pages/email/transport/list-connector-templates/index.js @@ -0,0 +1,110 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { RocketLaunch } from "@mui/icons-material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import ConnectorTemplateDetails from "../../../../components/CippComponents/ConnectorTemplateDetails"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Exchange Connector Templates"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveExConnectorTemplate", + data: { + ID: "GUID", + }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + children: (data) => , + actions: actions, + size: "lg", + }; + + const simpleColumns = ["name", "cippconnectortype", "GUID"]; + + return ( + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/email/connectors/list-connectors/add.jsx b/src/pages/email/transport/list-connectors/add.jsx similarity index 93% rename from src/pages/email/connectors/list-connectors/add.jsx rename to src/pages/email/transport/list-connectors/add.jsx index 5410d79c653d..8d173c49908b 100644 --- a/src/pages/email/connectors/list-connectors/add.jsx +++ b/src/pages/email/transport/list-connectors/add.jsx @@ -27,9 +27,9 @@ const AddPolicy = () => { return ( @@ -55,7 +55,7 @@ const AddPolicy = () => { formControl={formControl} multiple={false} api={{ - queryKey: `TemplateListTransport`, + queryKey: `TemplateListConnectors`, labelField: "name", valueField: (option) => option, url: "/api/ListExconnectorTemplates", diff --git a/src/pages/email/connectors/list-connectors/index.js b/src/pages/email/transport/list-connectors/index.js similarity index 82% rename from src/pages/email/connectors/list-connectors/index.js rename to src/pages/email/transport/list-connectors/index.js index 58799a4bd6dc..6f2f9fe11539 100644 --- a/src/pages/email/connectors/list-connectors/index.js +++ b/src/pages/email/transport/list-connectors/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { RocketLaunch, Book, Check, Block, Delete } from "@mui/icons-material"; import Link from "next/link"; const Page = () => { @@ -11,6 +12,7 @@ const Page = () => { label: "Create template based on connector", type: "POST", url: "/api/AddExConnectorTemplate", + icon: , postEntireRow: true, confirmText: "Are you sure you want to create a template based on this connector?", color: "info", @@ -19,8 +21,10 @@ const Page = () => { label: "Enable Connector", type: "POST", url: "/api/EditExConnector", + icon: , + condition: (row) => !row.Enabled, data: { - State: "Enable", + State: "!Enable", GUID: "Guid", Type: "cippconnectortype", }, @@ -31,8 +35,10 @@ const Page = () => { label: "Disable Connector", type: "POST", url: "/api/EditExConnector", + icon: , + condition: (row) => row.Enabled, data: { - State: "Disable", + State: "!Disable", GUID: "Guid", Type: "cippconnectortype", }, @@ -43,6 +49,7 @@ const Page = () => { label: "Delete Connector", type: "POST", url: "/api/RemoveExConnector", + icon: , data: { GUID: "Guid", Type: "cippconnectortype", @@ -77,13 +84,13 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} - titleButton={{ - label: "Deploy Connector", - href: "/email/connectors/deploy-connector", - }} cardButton={ <> - diff --git a/src/pages/email/transport/list-rules/index.js b/src/pages/email/transport/list-rules/index.js index 7312f49794fc..0c6128024c6e 100644 --- a/src/pages/email/transport/list-rules/index.js +++ b/src/pages/email/transport/list-rules/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; -import { Book, DoDisturb, Done } from "@mui/icons-material"; +import { Book, DoDisturb, Done, RocketLaunch } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; @@ -88,7 +88,11 @@ const Page = () => { ]} cardButton={ <> - diff --git a/src/pages/email/transport/list-templates/index.js b/src/pages/email/transport/list-templates/index.js index 1bfff7428f93..58340c19ff38 100644 --- a/src/pages/email/transport/list-templates/index.js +++ b/src/pages/email/transport/list-templates/index.js @@ -1,13 +1,62 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { TrashIcon } from "@heroicons/react/24/outline"; import { Button } from "@mui/material"; +import { RocketLaunch, GitHub } from "@mui/icons-material"; import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Transport Rule Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "POST", @@ -35,7 +84,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js new file mode 100644 index 000000000000..a381de6b12df --- /dev/null +++ b/src/pages/endpoint/MEM/devices/index.js @@ -0,0 +1,291 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { + Sync, + RestartAlt, + LocationOn, + Password, + PasswordOutlined, + Key, + Security, + FindInPage, + Shield, + Archive, + AutoMode, + Recycling, +} from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Devices"; + const tenantFilter = useSettings().currentTenant; + + const actions = [ + { + label: "View in InTune", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + { + label: "Sync Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "syncDevice", + }, + confirmText: "Are you sure you want to sync this device?", + }, + { + label: "Reboot Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "rebootNow", + }, + confirmText: "Are you sure you want to reboot this device?", + }, + { + label: "Locate Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "locateDevice", + }, + confirmText: "Are you sure you want to locate this device?", + }, + { + label: "Retrieve LAPs password", + type: "POST", + icon: , + url: "/api/ExecGetLocalAdminPassword", + data: { + GUID: "azureADDeviceId", + }, + confirmText: "Are you sure you want to retrieve the local admin password?", + }, + { + label: "Rotate Local Admin Password", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "RotateLocalAdminPassword", + }, + confirmText: "Are you sure you want to rotate the password for this device?", + }, + { + label: "Retrieve Bitlocker Keys", + type: "POST", + icon: , + url: "/api/ExecGetRecoveryKey", + data: { + GUID: "azureADDeviceId", + }, + confirmText: "Are you sure you want to retrieve the Bitlocker keys?", + }, + { + label: "Windows Defender Full Scan", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "WindowsDefenderScan", + quickScan: false, + }, + confirmText: "Are you sure you want to perform a full scan on this device?", + }, + { + label: "Windows Defender Quick Scan", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "WindowsDefenderScan", + quickScan: true, + }, + confirmText: "Are you sure you want to perform a quick scan on this device?", + }, + { + label: "Update Windows Defender", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "windowsDefenderUpdateSignatures", + }, + confirmText: "Are you sure you want to update the Windows Defender signatures for this device?", + }, + { + label: "Generate logs and ship to MEM", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "CreateDeviceLogCollectionRequest", + }, + confirmText: "Are you sure you want to generate logs and ship these to MEM?", + }, + /* + { + label: "Rename device", + type: "POST", + icon: null, + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "setDeviceName", + }, + confirmText: "Enter the new name for the device", + }, + */ + { + label: "Fresh Start (Remove user data)", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + }, + confirmText: "Are you sure you want to Fresh Start this device?", + }, + { + label: "Fresh Start (Do not remove user data)", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: true, + }, + confirmText: "Are you sure you want to Fresh Start this device?", + }, + { + label: "Wipe Device, keep enrollment data", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + keepEnrollmentData: true, + }, + confirmText: "Are you sure you want to wipe this device, and retain enrollment data?", + }, + { + label: "Wipe Device, remove enrollment data", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + keepEnrollmentData: false, + }, + confirmText: "Are you sure you want to wipe this device, and remove enrollment data?", + }, + { + label: "Wipe Device, keep enrollment data, and continue at powerloss", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepEnrollmentData: true, + keepUserData: false, + useProtectedWipe: true, + }, + confirmText: "Are you sure you want to wipe this device? This will retain enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", + }, + { + label: "Wipe Device, remove enrollment data, and continue at powerloss", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepEnrollmentData: false, + keepUserData: false, + useProtectedWipe: true, + }, + confirmText: "Are you sure you want to wipe this device? This will also remove enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", + }, + { + label: "Autopilot Reset", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "wipe", + keepUserData: "false", + keepEnrollmentData: "true", + }, + confirmText: "Are you sure you want to Autopilot Reset this device?", + }, + { + label: "Retire device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "retire", + }, + confirmText: "Are you sure you want to retire this device?", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["deviceName", "userPrincipalName"], + actions: actions, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js index 51eb0354d5f9..47e955ce1452 100644 --- a/src/pages/endpoint/MEM/list-compliance-policies/index.js +++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js @@ -13,7 +13,7 @@ const Page = () => { url: "/api/AddIntuneTemplate", data: { ID: "id", - URLName: "URLName", + ODataType: "@odata.type", }, confirmText: "Are you sure you want to create a template based on this policy?", icon: , diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js index 299881df53fe..21c2f1962a24 100644 --- a/src/pages/endpoint/MEM/list-policies/index.js +++ b/src/pages/endpoint/MEM/list-policies/index.js @@ -60,7 +60,7 @@ const Page = () => { }, { label: "Delete Policy", - type: "GET", + type: "POST", url: "/api/RemovePolicy", data: { ID: "id", diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx new file mode 100644 index 000000000000..eb2851b0544f --- /dev/null +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -0,0 +1,278 @@ +import { Layout as DashboardLayout } from "/src/layouts/index"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage"; +import { Code, TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; +import { showToast } from "/src/store/toasts"; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + IconButton, + CircularProgress, + DialogActions, +} from "@mui/material"; +import { CippCodeBlock } from "/src/components/CippComponents/CippCodeBlock"; +import { useState, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { Search, Close, Save } from "@mui/icons-material"; +import { useSettings } from "../../../../hooks/use-settings"; +import { Stack } from "@mui/system"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import axios from "axios"; + +const Page = () => { + const pageTitle = "Scripts"; + const [codeOpen, setCodeOpen] = useState(false); + const [codeContent, setCodeContent] = useState(""); + const [scriptId, setScriptId] = useState(null); + const [saveScript, setSaveScript] = useState(false); + const [codeContentChanged, setCodeContentChanged] = useState(false); + const [warnOpen, setWarnOpen] = useState(false); + const [currentScript, setCurrentScript] = useState(null); + + const dispatch = useDispatch(); + + const tenantFilter = useSettings().currentTenant; + const { + isLoading: scriptIsLoading, + isRefetching: scriptIsFetching, + refetch: scriptRefetch, + data, + } = useQuery({ + queryKey: ["script", { scriptId }], + queryFn: async () => { + const response = await fetch( + `/api/EditIntuneScript?TenantFilter=${tenantFilter}&ScriptId=${scriptId}` + ); + return response.json(); + }, + refetchOnWindowFocus: false, + enabled: false, + }); + + // Refetch the script on scriptId change + useEffect(() => { + if (scriptId) { + scriptRefetch().then(({ data }) => { + setCurrentScript(data); + const scriptBytes = Buffer.from(data.scriptContent, "base64"); + setCodeContent(scriptBytes.toString("ascii")); + }); + } + }, [scriptId, scriptRefetch]); + + const handleScriptEdit = async (row, action) => { + setScriptId(row.id); + setCodeOpen(!codeOpen); + }; + + const codeChange = (newValue, evt) => { + setCodeContent(newValue); + setCodeContentChanged(true); + }; + + const codeClosed = () => { + if (codeContentChanged) { + setWarnOpen(!warnOpen); + } else { + setCodeOpen(!codeOpen); + setCodeContentChanged(false); + setScriptId(null); + setCodeContent(""); + } + }; + + const { refetch: saveScriptRefetch, isFetching: isSaving } = useQuery({ + queryKey: ["saveScript"], + queryFn: async () => { + const scriptBytes = Buffer.from(codeContent, "ascii"); + const { + runAs32Bit, + id, + displayName, + description, + scriptContent, + runAsAccount, + fileName, + roleScopeTagIds + } = currentScript; + const patchData = { + TenantFilter: tenantFilter, + ScriptId: id, + IntuneScript: JSON.stringify({ + runAs32Bit, + id, + displayName, + description, + scriptContent: scriptBytes.toString("base64"), // Convert to base64 + runAsAccount, + fileName, + roleScopeTagIds, + }), + }; + + const response = await fetch("/api/EditIntuneScript", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patchData), + }); + + if (!response.ok) { + dispatch ( + showToast({ + title: "Script Save Error", + message: "Your Intune script could not be saved.", + type: "error", + }) + ); + } + + return response.json(); + }, + enabled: false, + refetchOnWindowFocus: false, + }); + + const queryClient = useQueryClient(); + + const saveCode = async () => { + const { data } = await saveScriptRefetch(); + setCodeContentChanged(false); + setCodeOpen(!codeOpen); + dispatch ( + showToast({ + title: "Script Saved", + message: "Your Intune script has been saved successfully.", + type: "update", + }) + ); + }; + + const actions = [ + { + label: "Edit Script", + icon: , + color: "primary", + noConfirm: true, + customFunction: handleScriptEdit, + }, + { + label: "Delete Script", + type: "POST", + url: "/api/RemoveIntuneScript", + data: { + ID: "id", + displayName: "displayName", + ScriptType: "scriptType", + }, + confirmText: "Are you sure you want to delete this script?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "scriptType", + "id", + "fileName", + "displayName", + "description", + "lastModifiedDateTime", + "runAsAccount", + "createdDateTime", + "runAs32Bit", + "executionFrequency", + "enforceSignatureCheck", + ], + actions: actions, + }; + + const simpleColumns = [ + "scriptType", + "displayName", + "description", + "runAsAccount", + "lastModifiedDateTime", + ]; + + return ( + <> + + + + + Script Content + {!isSaving && ( + + + + )} + {!isSaving && ( + + + + )} + {isSaving && ( + + )} + + + {(scriptIsFetching || scriptIsLoading) && } + {(!scriptIsFetching && !scriptIsLoading) && ( + + )} + + + + Confirmation + + Changes detected, are you sure you want to close? + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; \ No newline at end of file diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index e61d3930aedc..d87b2243c259 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -1,11 +1,16 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Available Endpoint Manager Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const actions = [ { label: "Edit Template Name and Description", @@ -30,6 +35,50 @@ const Page = () => { icon: , color: "info", }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "GET", diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx index 7bceb2e69857..254162f48e4b 100644 --- a/src/pages/endpoint/applications/list/add.jsx +++ b/src/pages/endpoint/applications/list/add.jsx @@ -176,7 +176,6 @@ const ApplicationDeploymentForm = () => { {selectedTenants?.map((tenant, index) => ( - {console.log(tenant)} { url: "/api/ExecAssignApp", data: { AssignTo: "AllUsers", - TenantFilter: "Tenant", ID: "id", }, confirmText: "Are you sure you want to assign this app to all users?", @@ -28,7 +27,6 @@ const Page = () => { url: "/api/ExecAssignApp", data: { AssignTo: "AllDevices", - TenantFilter: "Tenant", ID: "id", }, confirmText: "Are you sure you want to assign this app to all devices?", @@ -41,7 +39,6 @@ const Page = () => { url: "/api/ExecAssignApp", data: { AssignTo: "Both", - TenantFilter: "Tenant", ID: "id", }, confirmText: "Are you sure you want to assign this app to all users and devices?", @@ -53,7 +50,6 @@ const Page = () => { type: "POST", url: "/api/RemoveApp", data: { - TenantFilter: "Tenant", ID: "id", }, confirmText: "Are you sure you want to delete this application?", @@ -95,7 +91,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/applications/queue/index.js b/src/pages/endpoint/applications/queue/index.js index 0f30341cefcb..3e683b11e8b0 100644 --- a/src/pages/endpoint/applications/queue/index.js +++ b/src/pages/endpoint/applications/queue/index.js @@ -2,6 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { CheckmarkIcon } from "react-hot-toast"; import { Button } from "@mui/material"; +import { PlayArrow, Add } from "@mui/icons-material"; import Link from "next/link"; import { ApiPostCall } from "../../../../api/ApiCall"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; @@ -40,9 +41,11 @@ const Page = () => { tenantInTitle={false} cardButton={ <> - + - diff --git a/src/pages/endpoint/autopilot/add-status-page/index.js b/src/pages/endpoint/autopilot/add-status-page/index.js index 2ee87af41a8e..1a3454035a19 100644 --- a/src/pages/endpoint/autopilot/add-status-page/index.js +++ b/src/pages/endpoint/autopilot/add-status-page/index.js @@ -1,14 +1,120 @@ - +import React from "react"; +import { Grid, Divider } from "@mui/material"; +import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; const Page = () => { - const pageTitle = "Add Status Page"; + const formControl = useForm({ + mode: "onChange", + defaultValues: { + TimeOutInMinutes: "", + ErrorMessage: "", + ShowProgress: false, + EnableLog: false, + OBEEOnly: false, + blockDevice: false, + Allowretry: false, + AllowReset: false, + AllowFail: false, + }, + }); return ( -
-

{pageTitle}

-

This is a placeholder page for the add status page section.

-
+ + + {/* Tenant Selector */} + + + + + + + {/* Form Fields */} + + + + + + + + + {/* Switches */} + + + + + + + + + + + ); }; diff --git a/src/pages/endpoint/autopilot/list-devices/index.js b/src/pages/endpoint/autopilot/list-devices/index.js index b738be0ba6af..038e5b9d8d2a 100644 --- a/src/pages/endpoint/autopilot/list-devices/index.js +++ b/src/pages/endpoint/autopilot/list-devices/index.js @@ -1,14 +1,20 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; import { Button } from "@mui/material"; +import { PersonAdd, Delete, Sync, Add } from "@mui/icons-material"; +import { useDialog } from "../../../../hooks/use-dialog"; import Link from "next/link"; +import { useState } from "react"; const Page = () => { const pageTitle = "Autopilot Devices"; + const createDialog = useDialog(); const actions = [ { label: "Assign device", + icon: , type: "POST", url: "/api/ExecAssignAPDevice", data: { @@ -38,6 +44,7 @@ const Page = () => { }, { label: "Delete Device", + icon: , type: "POST", url: "/api/RemoveAPDevice", data: { ID: "id" }, @@ -67,20 +74,36 @@ const Page = () => { ]; return ( - - - - } - /> + <> + + + + + } + /> + + ); }; diff --git a/src/pages/endpoint/autopilot/list-profiles/index.js b/src/pages/endpoint/autopilot/list-profiles/index.js index 2e9d5f419aef..e1e3e8375b1a 100644 --- a/src/pages/endpoint/autopilot/list-profiles/index.js +++ b/src/pages/endpoint/autopilot/list-profiles/index.js @@ -3,6 +3,7 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { EyeIcon } from "@heroicons/react/24/outline"; import { Button } from "@mui/material"; import Link from "next/link"; +import { AccountCircle } from "@mui/icons-material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; const Page = () => { @@ -32,7 +33,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/autopilot/list-status-pages/add.jsx b/src/pages/endpoint/autopilot/list-status-pages/add.jsx deleted file mode 100644 index 328283d07829..000000000000 --- a/src/pages/endpoint/autopilot/list-status-pages/add.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react"; -import { Grid, Divider } from "@mui/material"; -import { useForm, useWatch } from "react-hook-form"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; - -const AutopilotStatusPageForm = () => { - const formControl = useForm({ - mode: "onChange", - defaultValues: { - TimeOutInMinutes: "", - ErrorMessage: "", - ShowProgress: false, - EnableLog: false, - OBEEOnly: false, - blockDevice: false, - Allowretry: false, - AllowReset: false, - AllowFail: false, - }, - }); - - return ( - - - {/* Tenant Selector */} - - - - - - - {/* Form Fields */} - - - - - - - - - {/* Switches */} - - - - - - - - - - - - ); -}; - -AutopilotStatusPageForm.getLayout = (page) => {page}; - -export default AutopilotStatusPageForm; diff --git a/src/pages/endpoint/autopilot/list-status-pages/index.js b/src/pages/endpoint/autopilot/list-status-pages/index.js index 939685bd69ad..fc1525f4cbbb 100644 --- a/src/pages/endpoint/autopilot/list-status-pages/index.js +++ b/src/pages/endpoint/autopilot/list-status-pages/index.js @@ -2,6 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import { PostAdd } from "@mui/icons-material"; const Page = () => { const pageTitle = "Autopilot Status Pages"; @@ -25,7 +26,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/reports/devices/index.js b/src/pages/endpoint/reports/devices/index.js deleted file mode 100644 index 4ce9ebcbc46a..000000000000 --- a/src/pages/endpoint/reports/devices/index.js +++ /dev/null @@ -1,143 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { useSettings } from "/src/hooks/use-settings"; -import { EyeIcon } from "@heroicons/react/24/outline"; - -const Page = () => { - const pageTitle = "Devices"; - const tenantFilter = useSettings().currentTenant; - - const actions = [ - { - label: "Sync Device", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "syncDevice", - }, - confirmText: "Are you sure you want to sync this device?", - }, - { - label: "Reboot Device", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "rebootNow", - }, - confirmText: "Are you sure you want to reboot this device?", - }, - { - label: "Locate Device", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "locateDevice", - }, - confirmText: "Are you sure you want to locate this device?", - }, - { - label: "Retrieve LAPs password", - type: "POST", - url: "/api/ExecGetLocalAdminPassword", - data: { - GUID: "azureADDeviceId", - }, - confirmText: "Are you sure you want to retrieve the local admin password?", - }, - { - label: "Rotate Local Admin Password", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "RotateLocalAdminPassword", - }, - confirmText: "Are you sure you want to rotate the password for this device?", - }, - { - label: "Retrieve Bitlocker Keys", - type: "POST", - url: "/api/ExecGetRecoveryKey", - data: { - GUID: "azureADDeviceId", - }, - confirmText: "Are you sure you want to retrieve the Bitlocker keys?", - }, - { - label: "Windows Defender Full Scan", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "WindowsDefenderScan", - quickScan: false, - }, - confirmText: "Are you sure you want to perform a full scan on this device?", - }, - { - label: "Windows Defender Quick Scan", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "WindowsDefenderScan", - quickScan: true, - }, - confirmText: "Are you sure you want to perform a quick scan on this device?", - }, - { - label: "Update Windows Defender", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "windowsDefenderUpdateSignatures", - }, - confirmText: - "Are you sure you want to update the Windows Defender signatures for this device?", - }, - { - label: "View in InTune", - link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, - color: "info", - icon: , - target: "_blank", - multiPost: false, - external: true, - }, - ]; - - const offCanvas = { - extendedInfoFields: ["deviceName", "userPrincipalName"], - actions: actions, - }; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/identity/administration/deleted-items/index.js b/src/pages/identity/administration/deleted-items/index.js index ab568cfb92d9..43d9ddb9d817 100644 --- a/src/pages/identity/administration/deleted-items/index.js +++ b/src/pages/identity/administration/deleted-items/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash'; const Page = () => { const pageTitle = "Deleted Items"; @@ -8,8 +9,9 @@ const Page = () => { { label: "Restore Object", type: "GET", + icon: , url: "/api/ExecRestoreDeleted", - data: { TenantFilter: "Tenant", ID: "id" }, + data: { ID: "id" }, confirmText: "Are you sure you want to restore this user?", multiPost: false, }, diff --git a/src/pages/identity/administration/devices/index.js b/src/pages/identity/administration/devices/index.js index 456329c33755..0ae68838f469 100644 --- a/src/pages/identity/administration/devices/index.js +++ b/src/pages/identity/administration/devices/index.js @@ -1,7 +1,7 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import { useSettings } from "/src/hooks/use-settings"; -import { EyeIcon } from "@heroicons/react/24/outline"; +import { Visibility, CheckCircleOutline, Block, VpnKey, DeleteForever } from "@mui/icons-material"; const Page = () => { const pageTitle = "Devices"; @@ -12,7 +12,7 @@ const Page = () => { label: "View in Entra", link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_Devices/DeviceDetailsMenuBlade/~/Properties/objectId/[id]/deviceId/`, color: "info", - icon: , + icon: , target: "_blank", multiPost: false, external: true, @@ -27,6 +27,7 @@ const Page = () => { }, confirmText: "Are you sure you want to enable this device?", multiPost: false, + icon: , }, { label: "Disable Device", @@ -38,6 +39,7 @@ const Page = () => { }, confirmText: "Are you sure you want to disable this device?", multiPost: false, + icon: , }, { label: "Retrieve BitLocker Keys", @@ -48,6 +50,7 @@ const Page = () => { }, confirmText: "Are you sure you want to retrieve the BitLocker keys?", multiPost: false, + icon: , }, { label: "Delete Device", @@ -59,6 +62,7 @@ const Page = () => { }, confirmText: "Are you sure you want to delete this device?", multiPost: false, + icon: , }, ]; diff --git a/src/pages/identity/administration/group-templates/add.jsx b/src/pages/identity/administration/group-templates/add.jsx index 808fa0b81985..fe5b597fe926 100644 --- a/src/pages/identity/administration/group-templates/add.jsx +++ b/src/pages/identity/administration/group-templates/add.jsx @@ -17,6 +17,7 @@ const Page = () => { return ( <> { const pageTitle = "Group Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "GET", url: "/api/RemoveGroupTemplate", + icon: , data: { ID: "GUID", }, @@ -31,10 +81,10 @@ const Page = () => { actions={actions} cardButton={ <> - - diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index 1fb57be03d1e..03d36a08a198 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -2,8 +2,8 @@ import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import Link from "next/link"; -import { EyeIcon, LockClosedIcon, LockOpenIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { LockOpen, Visibility, VisibilityOff } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { Visibility, VisibilityOff, GroupAdd, Edit, LockOpen, Lock } from "@mui/icons-material"; const Page = () => { const pageTitle = "Groups"; @@ -13,7 +13,7 @@ const Page = () => { label: "Edit Group", link: "/identity/administration/groups/edit?groupId=[id]", multiPost: false, - icon: , + icon: , color: "success", }, { @@ -22,7 +22,6 @@ const Page = () => { url: "/api/ExecGroupsHideFromGAL", icon: , data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", HidefromGAL: true, @@ -37,7 +36,6 @@ const Page = () => { url: "/api/ExecGroupsHideFromGAL", icon: , data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", }, @@ -49,9 +47,8 @@ const Page = () => { label: "Only allow messages from people inside the organisation", type: "GET", url: "/api/ExecGroupsDeliveryManagement", - icon: , + icon: , data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", OnlyAllowInternal: true, @@ -63,10 +60,9 @@ const Page = () => { { label: "Allow messages from people inside and outside the organisation", type: "GET", - icon: , + icon: , url: "/api/ExecGroupsDeliveryManagement", data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", }, @@ -106,22 +102,12 @@ const Page = () => { title={pageTitle} cardButton={ <> - } - apiUrl="/api/ListGraphRequest" - apiData={{ - Endpoint: "groups", - $select: - "id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName,assignedLicenses", - $count: true, - $orderby: "displayName", - $top: 999, - manualPagination: true, - }} - apiDataKey="Results" + apiUrl="/api/ListGroups" actions={actions} offCanvas={offCanvas} simpleColumns={[ diff --git a/src/pages/identity/administration/jit-admin/index.js b/src/pages/identity/administration/jit-admin/index.js index cc90ab853599..e4ad1c00ce46 100644 --- a/src/pages/identity/administration/jit-admin/index.js +++ b/src/pages/identity/administration/jit-admin/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button } from "@mui/material"; +import { AdminPanelSettings } from "@mui/icons-material"; import Link from "next/link"; const Page = () => { @@ -8,7 +9,7 @@ const Page = () => { - diff --git a/src/pages/identity/administration/risky-users/index.js b/src/pages/identity/administration/risky-users/index.js index 759117480d8b..a5327f24d7f3 100644 --- a/src/pages/identity/administration/risky-users/index.js +++ b/src/pages/identity/administration/risky-users/index.js @@ -1,7 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Clear } from "@mui/icons-material"; -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { Clear, Search } from "@mui/icons-material"; const Page = () => { const pageTitle = "Risky Users"; @@ -19,7 +18,7 @@ const Page = () => { { label: "Research Compromised Account", type: "GET", - icon: , + icon: , link: "/identity/administration/users/user/bec?userId=[id]", confirmText: "Are you sure you want to research this compromised account?", multiPost: false, diff --git a/src/pages/identity/administration/users/add.jsx b/src/pages/identity/administration/users/add.jsx index 5d15605280e7..6db4b2a4bbf6 100644 --- a/src/pages/identity/administration/users/add.jsx +++ b/src/pages/identity/administration/users/add.jsx @@ -11,7 +11,7 @@ const Page = () => { const userSettingsDefaults = useSettings(); const formControl = useForm({ - mode: "onChange", + mode: "onBlur", defaultValues: { tenantFilter: userSettingsDefaults.currentTenant, usageLocation: userSettingsDefaults.usageLocation, @@ -36,6 +36,7 @@ const Page = () => { newFields.usageLocation = { label: usageLocation, value: usageLocation }; } newFields.tenantFilter = userSettingsDefaults.currentTenant; + formControl.reset(newFields); } }, [formValues]); diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index c03a62bb56cd..0b67a7ece144 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -1,6 +1,7 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { Button } from "@mui/material"; +import { Send, GroupAdd, PersonAdd } from "@mui/icons-material"; import Link from "next/link"; import { useSettings } from "/src/hooks/use-settings.js"; import { CippUserActions } from "/src/components/CippComponents/CippUserActions.jsx"; @@ -9,9 +10,28 @@ const Page = () => { const pageTitle = "Users"; const tenant = useSettings().currentTenant; + const filters = [ + { + filterName: "Account Enabled", + value: [{ id: "accountEnabled", value: "Yes" }], + type: "column", + }, + { + filterName: "Account Disabled", + value: [{ id: "accountEnabled", value: "No" }], + type: "column", + }, + { + filterName: "Guest Accounts", + value: [{ id: "userType", value: "Guest" }], + type: "column", + }, + ]; + const offCanvas = { extendedInfoFields: [ "createdDateTime", // Created Date (UTC) + "id", // Unique ID "userPrincipalName", // UPN "givenName", // Given Name "surname", // Surname @@ -23,7 +43,7 @@ const Page = () => { "city", // City "department", // Department "onPremisesLastSyncDateTime", // OnPrem Last Sync - "id", // Unique ID + "onPremisesDistinguishedName", // OnPrem DN "otherMails", // Alternate Email Addresses ], actions: CippUserActions(), @@ -35,22 +55,22 @@ const Page = () => { apiUrl="/api/ListGraphRequest" cardButton={ <> - + - - } apiData={{ Endpoint: "users", manualPagination: true, $select: - "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,onPremisesDistinguishedName,officeLocation,onPremisesLastSyncDateTime,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled", + "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", $count: true, $orderby: "displayName", $top: 999, @@ -67,24 +87,7 @@ const Page = () => { "proxyAddresses", "assignedLicenses", ]} - filters={[ - { - filterName: "Account Enabled", - //true or false filters by yes/no - value: [{ id: "accountEnabled", value: "Yes" }], - type: "column", - }, - { - filterName: "Account Disabled", - value: [{ id: "accountEnabled", value: "No" }], - type: "column", - }, - { - filterName: "Guest Accounts", - value: [{ id: "userType", value: "Guest" }], - type: "column", - }, - ]} + filters={filters} /> ); }; diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index 88619b41fc13..ed7c586489ec 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { CheckCircle, Download, Mail } from "@mui/icons-material"; +import { CheckCircle, Download, Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; @@ -111,7 +111,7 @@ const Page = () => { const getUserMessage = () => { if (!becPollingCall.data) return null; if (becPollingCall.data.NewUsers && becPollingCall.data.NewUsers.length > 0) { - return "Suspicious new users have been found in the last 14 days. Please review the list below and take action as needed."; + return "New users have been found in the last 14 days. Please review the list below and take action as needed."; } return "No new users found."; }; @@ -126,7 +126,7 @@ const Page = () => { if (hasPotentialBreach) { return "Potential Breach found."; } - return "Suspicious new applications have been found. Please review the list below and take action as needed."; + return "New applications have been found. Please review the list below and take action as needed."; } return "No new applications found."; }; @@ -137,39 +137,52 @@ const Page = () => { becPollingCall.data.MailboxPermissionChanges && becPollingCall.data.MailboxPermissionChanges.length > 0 ) { - return "Suspicious mailbox permission changes have been found."; + return "Mailbox permission changes have been found."; } return "No mailbox permission changes found."; }; + const subtitle = userRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Created: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + return ( , - text: ( - - ), - }, - { - icon: , - text: ( - <> - Created:{" "} - - - ), - }, - ] - : [] - } + subtitle={subtitle} isFetching={userRequest.isFetching} > {/* Loading State: Show only Remediation Card and Check 1 with Loading Skeleton */} diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index 6bda3f8534c2..d11ba05bc1ba 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -4,7 +4,7 @@ import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Mail, Forward } from "@mui/icons-material"; +import { Mail, Forward, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; @@ -41,6 +41,10 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( @@ -49,6 +53,21 @@ const Page = () => { ), }, + { + icon: , + text: ( + + ), + }, ] : []; diff --git a/src/pages/identity/administration/users/user/devices.jsx b/src/pages/identity/administration/users/user/devices.jsx index b0d5d9649b89..23cda503df3a 100644 --- a/src/pages/identity/administration/users/user/devices.jsx +++ b/src/pages/identity/administration/users/user/devices.jsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Mail } from "@mui/icons-material"; +import { Check, Mail, Fingerprint } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; @@ -50,6 +50,10 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( diff --git a/src/pages/identity/administration/users/user/edit.jsx b/src/pages/identity/administration/users/user/edit.jsx index 8cd113275022..536c5184c216 100644 --- a/src/pages/identity/administration/users/user/edit.jsx +++ b/src/pages/identity/administration/users/user/edit.jsx @@ -9,11 +9,13 @@ import { useEffect } from "react"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import { getCippLicenseTranslation } from "/src/utils/get-cipp-license-translation"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Mail } from "@mui/icons-material"; +import { Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; +import { Button } from "@mui/material"; +import { Box } from "@mui/system"; const Page = () => { const userSettingsDefaults = useSettings(); const router = useRouter(); @@ -25,7 +27,7 @@ const Page = () => { }); const formControl = useForm({ - mode: "onChange", + mode: "onBlur", defaultValues: { tenantFilter: userSettingsDefaults.currentTenant, }, @@ -34,8 +36,16 @@ const Page = () => { useEffect(() => { if (userRequest.isSuccess) { const user = userRequest.data?.[0]; + //if we have userSettingsDefaults.userAttributes set, grab the .label from each userSsettingsDefaults, then set defaultAttributes.${label}.value to user.${label} + let defaultAttributes = {}; + if (userSettingsDefaults.userAttributes) { + userSettingsDefaults.userAttributes.forEach((attribute) => { + defaultAttributes[attribute.label] = { Value: user?.[attribute.label] }; + }); + } formControl.reset({ ...user, + defaultAttributes: defaultAttributes, tenantFilter: userSettingsDefaults.currentTenant, licenses: user.assignedLicenses.map((license) => ({ label: getCippLicenseTranslation([license]), @@ -55,14 +65,33 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( <> - Created + Created: ), }, + { + icon: , + text: ( + + ), + }, ] : []; @@ -84,11 +113,13 @@ const Page = () => { > {userRequest.isLoading && } {userRequest.isSuccess && ( - + + + )} diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 29082e3b6fb8..9b22c6ec415f 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Error, Mail } from "@mui/icons-material"; +import { Check, Error, Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; @@ -101,14 +101,33 @@ const Page = () => { ), }, + { + icon: , + text: , + }, { icon: , text: ( <> - Created + Created: ), }, + { + icon: , + text: ( + + ), + }, ] : []; diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 984371102e4f..0d8628fdd9d5 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { AdminPanelSettings, Check, Group, Mail } from "@mui/icons-material"; +import { AdminPanelSettings, Check, Group, Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; @@ -122,14 +122,33 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( <> - Created + Created: ), }, + { + icon: , + text: ( + + ), + }, ] : []; diff --git a/src/pages/identity/reports/azure-ad-connect-report/index.js b/src/pages/identity/reports/azure-ad-connect-report/index.js index 529ce0333b4e..97594a13d6ac 100644 --- a/src/pages/identity/reports/azure-ad-connect-report/index.js +++ b/src/pages/identity/reports/azure-ad-connect-report/index.js @@ -9,9 +9,6 @@ const simpleColumns = [ ]; const apiUrl = "/api/ListAzureADConnectStatus"; -const actions = []; // No actions specified in the original code - -const offCanvas = null; // No off-canvas details provided const Page = () => { return ( @@ -21,8 +18,6 @@ const Page = () => { apiData={{ DataToReturn: "AzureADObjectsInError", }} - actions={actions} - offCanvas={offCanvas} simpleColumns={simpleColumns} /> ); diff --git a/src/pages/identity/reports/mfa-report/index.js b/src/pages/identity/reports/mfa-report/index.js index fd18f1bdef0c..206e8337e2ef 100644 --- a/src/pages/identity/reports/mfa-report/index.js +++ b/src/pages/identity/reports/mfa-report/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { LockPerson } from "@mui/icons-material"; const Page = () => { const pageTitle = "MFA Report"; @@ -15,19 +16,71 @@ const Page = () => { "MFAMethods", "CAPolicies", ]; + const filters = [ + { + filterName: "Enabled, licensed users", + value: [ + { id: "AccountEnabled", value: "Yes" }, + { id: "isLicensed", value: "Yes" }, + ], + type: "column", + }, + { + filterName: "Enabled, licensed users missing MFA", + value: [ + { id: "AccountEnabled", value: "Yes" }, + { id: "isLicensed", value: "Yes" }, + { id: "MFARegistration", value: "No" }, + ], + type: "column", + }, + { + filterName: "No MFA methods registered", + value: [{ id: "MFARegistration", value: "No" }], + type: "column", + }, + { + filterName: "MFA methods registered", + value: [{ id: "MFARegistration", value: "Yes" }], + type: "column", + }, + ]; - /* Filters not supported in the current structure, need dev attention for integration. - filterlist: [ - { filterName: 'Enabled users', filter: '"accountEnabled":true' }, - { filterName: 'Non-guest users', filter: 'Complex: UPN notlike #EXT#' }, - { filterName: 'Licensed users', filter: 'Complex: IsLicensed eq true' }, - { filterName: 'Enabled, licensed non-guest users missing MFA', filter: 'Complex: UPN notlike #EXT#; IsLicensed eq true; accountEnabled eq true; MFARegistration ne true' }, - { filterName: 'No MFA methods registered', filter: 'Complex: MFARegistration ne true' }, - { filterName: 'MFA methods registered', filter: 'Complex: MFARegistration eq true' }, - ], - */ + const actions = [ + { + label: "Set Per-User MFA", + type: "POST", + icon: , + url: "/api/ExecPerUserMFA", + data: { userId: "UPN" }, + fields: [ + { + type: "autoComplete", + name: "State", + label: "State", + options: [ + { label: "Enforced", value: "Enforced" }, + { label: "Enabled", value: "Enabled" }, + { label: "Disabled", value: "Disabled" }, + ], + multiple: false, + creatable: false, + }, + ], + confirmText: "Are you sure you want to set per-user MFA for these users?", + multiPost: false, + }, + ]; - return ; + return ( + + ); }; Page.getLayout = (page) => {page}; diff --git a/src/pages/identity/reports/risk-detections/index.js b/src/pages/identity/reports/risk-detections/index.js index f6fe5e6b07da..3c54fc260e31 100644 --- a/src/pages/identity/reports/risk-detections/index.js +++ b/src/pages/identity/reports/risk-detections/index.js @@ -23,8 +23,7 @@ const Page = () => { "userDisplayName", "userPrincipalName", "detectedDateTime", - "location.city", - "location.countryOrRegion", + "location", "ipAddress", "riskLevel", "riskState", @@ -39,8 +38,7 @@ const Page = () => { const simpleColumns = [ "detectedDateTime", "userPrincipalName", - "location.city", - "location.countryOrRegion", + "location", "ipAddress", "riskState", "riskDetail", diff --git a/src/pages/identity/reports/signin-report/index.js b/src/pages/identity/reports/signin-report/index.js index 6304b8fa0da6..619f99ef8bcd 100644 --- a/src/pages/identity/reports/signin-report/index.js +++ b/src/pages/identity/reports/signin-report/index.js @@ -7,8 +7,6 @@ import CippButtonCard from "/src/components/CippCards/CippButtonCard"; const Page = () => { const pageTitle = "Sign Ins Report"; const apiUrl = "/api/ListSignIns"; - const actions = []; - const offCanvas = null; const simpleColumns = [ "createdDateTime", "userPrincipalName", @@ -16,6 +14,7 @@ const Page = () => { "authenticationRequirement", "errorCode", "additionalDetails", + "ipAddress", "locationcipp", ]; @@ -102,8 +101,6 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} apiData={appliedFilters} - actions={actions} - offCanvas={offCanvas} simpleColumns={simpleColumns} queryKey={`ListSignIns-${JSON.stringify(appliedFilters)}`} /> diff --git a/src/pages/index.js b/src/pages/index.js index 0c60b06fe5a2..c5810877c598 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -167,6 +167,7 @@ const Page = () => { label: portal.label, target: "_blank", link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), + icon: portal.icon, })); setPortalMenuItems(menuItems); } diff --git a/src/pages/onboarding.js b/src/pages/onboarding.js index ffe3018ddd58..ee61bbe28261 100644 --- a/src/pages/onboarding.js +++ b/src/pages/onboarding.js @@ -24,8 +24,7 @@ const Page = () => { value: "CreateApp", }, { - description: - "I would like to refresh my token or replace the user I've used for my previous token.", + description: "I would like to refresh my token or replace the account I've used.", icon: , label: "Refresh Tokens for existing application ", value: "UpdateTokens", diff --git a/src/pages/security/incidents/list-alerts/index.js b/src/pages/security/incidents/list-alerts/index.js index 09e1411d9d3b..444459c102ec 100644 --- a/src/pages/security/incidents/list-alerts/index.js +++ b/src/pages/security/incidents/list-alerts/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Assignment, Done } from "@mui/icons-material"; const Page = () => { const pageTitle = "Alerts List"; @@ -9,9 +10,9 @@ const Page = () => { { label: "Set status to in progress", type: "POST", + icon: , url: "/api/ExecSetSecurityAlert", data: { - TenantFilter: "Tenant", GUID: "id", Status: "inProgress", Vendor: "vendorInformation.vendor", @@ -22,9 +23,9 @@ const Page = () => { { label: "Set status to resolved", type: "POST", + icon: , url: "/api/ExecSetSecurityAlert", data: { - TenantFilter: "Tenant", GUID: "id", Status: "resolved", Vendor: "vendorInformation.vendor", @@ -61,6 +62,7 @@ const Page = () => { { const pageTitle = "Incidents List"; @@ -9,9 +10,9 @@ const Page = () => { { label: "Assign to self", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", GUID: "id", Assigned: "currentUserId", }, @@ -20,9 +21,9 @@ const Page = () => { { label: "Set status to active", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", GUID: "id", Status: "active", Assigned: "currentAssignedUser", @@ -32,9 +33,9 @@ const Page = () => { { label: "Set status to in progress", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", GUID: "id", Status: "inProgress", Assigned: "currentAssignedUser", @@ -44,9 +45,9 @@ const Page = () => { { label: "Set status to resolved", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", GUID: "id", Status: "resolved", Assigned: "currentAssignedUser", @@ -82,7 +83,7 @@ const Page = () => { { const pageTitle = "OneDrive"; @@ -7,6 +8,7 @@ const Page = () => { const actions = [ { label: "Add permissions to OneDrive", + icon: , type: "POST", url: "/api/ExecSharePointPerms", data: { @@ -36,6 +38,7 @@ const Page = () => { }, { label: "Remove permissions from OneDrive", + icon: , type: "POST", url: "/api/ExecSharePointPerms", data: { @@ -65,17 +68,11 @@ const Page = () => { }, ]; - const offCanvas = { - extendedInfoFields: ["UPN"], - actions: actions, - }; - return ( { @@ -10,6 +18,7 @@ const Page = () => { { label: "Add Member", type: "POST", + icon: , url: "/api/ExecSetSharePointMember", data: { groupId: "ownerPrincipalName", @@ -40,6 +49,7 @@ const Page = () => { { label: "Remove Member", type: "POST", + icon: , url: "/api/ExecSetSharePointMember", data: { groupId: "ownerPrincipalName", @@ -70,6 +80,7 @@ const Page = () => { { label: "Add Site Admin", type: "POST", + icon: , url: "/api/ExecSharePointPerms", data: { UPN: "ownerPrincipalName", @@ -99,6 +110,7 @@ const Page = () => { { label: "Remove Site Admin", type: "POST", + icon: , url: "/api/ExecSharePointPerms", data: { UPN: "ownerPrincipalName", @@ -151,10 +163,14 @@ const Page = () => { ]} cardButton={ <> - - diff --git a/src/pages/teams-share/teams/business-voice/index.js b/src/pages/teams-share/teams/business-voice/index.js index 725a2a8a5331..6cef4fd00045 100644 --- a/src/pages/teams-share/teams/business-voice/index.js +++ b/src/pages/teams-share/teams/business-voice/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { PersonAdd, PersonRemove, LocationOn } from "@mui/icons-material"; const Page = () => { const pageTitle = "Teams Business Voice"; @@ -9,6 +10,7 @@ const Page = () => { { label: "Assign User", type: "POST", + icon: , url: "/api/ExecTeamsVoicePhoneNumberAssignment", data: { PhoneNumber: "TelephoneNumber", @@ -34,6 +36,7 @@ const Page = () => { { label: "Unassign User", type: "POST", + icon: , url: "/api/ExecRemoveTeamsVoicePhoneNumberAssignment", data: { PhoneNumber: "TelephoneNumber", @@ -45,6 +48,7 @@ const Page = () => { { label: "Set Emergency Location", type: "POST", + icon: , url: "/api/ExecTeamsVoicePhoneNumberAssignment", data: { PhoneNumber: "TelephoneNumber", diff --git a/src/pages/teams-share/teams/list-team/index.js b/src/pages/teams-share/teams/list-team/index.js index db874b1091da..fb3580cc2622 100644 --- a/src/pages/teams-share/teams/list-team/index.js +++ b/src/pages/teams-share/teams/list-team/index.js @@ -1,7 +1,9 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { GroupAdd } from "@mui/icons-material"; import Link from "next/link"; +import { Edit } from "@mui/icons-material"; const Page = () => { const pageTitle = "Teams"; @@ -12,6 +14,7 @@ const Page = () => { link: "/identity/administration/groups/edit?groupId=[id]", multiPost: false, color: "warning", + icon: , }, ]; @@ -23,7 +26,7 @@ const Page = () => { simpleColumns={["displayName", "description", "visibility", "mailNickname", "id"]} cardButton={ <> - diff --git a/src/pages/tenant/administration/alert-configuration/index.js b/src/pages/tenant/administration/alert-configuration/index.js index 6b68555d0e66..1544d84c8e39 100644 --- a/src/pages/tenant/administration/alert-configuration/index.js +++ b/src/pages/tenant/administration/alert-configuration/index.js @@ -3,7 +3,7 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import Link from "next/link"; import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete } from "@mui/icons-material"; +import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material"; const Page = () => { const pageTitle = "Alerts"; @@ -43,7 +43,11 @@ const Page = () => { apiUrl="/api/ListAlertsQueue" tenantInTitle={false} cardButton={ - } diff --git a/src/pages/tenant/administration/authentication-methods/index.js b/src/pages/tenant/administration/authentication-methods/index.js index 7da41b844058..d86eccb0c3e6 100644 --- a/src/pages/tenant/administration/authentication-methods/index.js +++ b/src/pages/tenant/administration/authentication-methods/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Check, Block } from "@mui/icons-material"; const Page = () => { const pageTitle = "Auth Methods"; @@ -12,16 +13,18 @@ const Page = () => { { label: "Enable Policy", type: "POST", + icon: , url: "/api/SetAuthMethod", - data: { state: "enabled", id: "id" }, + data: { state: "!enabled", id: "id" }, confirmText: "Are you sure you want to enable this policy?", multiPost: false, }, { label: "Disable Policy", type: "POST", + icon: , url: "/api/SetAuthMethod", - data: { state: "disabled", id: "id" }, + data: { state: "!disabled", id: "id" }, confirmText: "Are you sure you want to disable this policy?", multiPost: false, }, diff --git a/src/pages/tenant/administration/enterprise-apps/index.js b/src/pages/tenant/administration/enterprise-apps/index.js index 58fa6b2ddf5f..258e09ce23d8 100644 --- a/src/pages/tenant/administration/enterprise-apps/index.js +++ b/src/pages/tenant/administration/enterprise-apps/index.js @@ -1,11 +1,33 @@ // this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83 import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Launch } from "@mui/icons-material"; const Page = () => { const pageTitle = "Enterprise Applications"; const apiUrl = "/api/ListGraphRequest"; - const actions = []; + + const actions = [ + { + icon: , + label: "View Application", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Overview/objectId/[id]/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + }, + { + icon: , + label: "View App Registration", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + condition: (row) => row.tags.includes("WindowsAzureActiveDirectoryIntegratedApp"), + }, + ]; const offCanvas = { extendedInfoFields: [ @@ -31,7 +53,7 @@ const Page = () => { const apiParams = { Endpoint: "servicePrincipals", $select: - "appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags", + "id,appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags", $count: true, $top: 999, }; diff --git a/src/pages/tenant/administration/securescore/index.js b/src/pages/tenant/administration/securescore/index.js index 432b4946e82f..4f86d718a7a9 100644 --- a/src/pages/tenant/administration/securescore/index.js +++ b/src/pages/tenant/administration/securescore/index.js @@ -8,7 +8,6 @@ import { CheckCircleIcon, GlobeAltIcon } from "@heroicons/react/24/outline"; import { Map, Score } from "@mui/icons-material"; import { CippChartCard } from "../../../../components/CippCards/CippChartCard"; import TimeAgo from "javascript-time-ago"; -import en from "javascript-time-ago/locale/en.json"; import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; import DOMPurify from "dompurify"; import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; @@ -26,8 +25,15 @@ const Page = () => { const [updatesData, setUpdatesData] = useState({ data: {}, ready: false }); const cippTableDialog = useDialog(); - TimeAgo.addLocale(en); const timeAgo = new TimeAgo("en-US"); + + const openRemediation = (url) => { + if (url.startsWith("https")) { + window.open(url, "_blank"); + } else { + navigate(url); + } + }; return ( { CardButton={ <> - + {secureScoreControl.controlStateUpdates?.length > 0 && ( - @@ -23,7 +28,13 @@ const Page = () => { showHidden: true, Type: "New-CIPPBackup", }} - simpleColumns={["Tenant", "TaskState", "ExecutedTime"]} + simpleColumns={[ + "Tenant", + "Name", + "Parameters.ScheduledBackupValues", + "TaskState", + "ExecutedTime", + ]} actions={[ { label: "Delete Task", @@ -34,7 +45,7 @@ const Page = () => { }, ]} offCanvas={{ - extendedInfoFields: ["RowKey", "TaskState", "ExecutedTime"], + extendedInfoFields: ["Name", "Tenant", "TaskState", "ExecutedTime"], actions: [ { label: "Delete Task", diff --git a/src/pages/tenant/conditional/deploy-vacation/index.js b/src/pages/tenant/conditional/deploy-vacation/index.js index ddddd13809c2..ca4092689195 100644 --- a/src/pages/tenant/conditional/deploy-vacation/index.js +++ b/src/pages/tenant/conditional/deploy-vacation/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button } from "@mui/material"; +import { EventAvailable } from "@mui/icons-material"; import Link from "next/link"; const Page = () => { @@ -8,7 +9,7 @@ const Page = () => { - diff --git a/src/pages/tenant/conditional/list-named-locations/index.js b/src/pages/tenant/conditional/list-named-locations/index.js index 877d38a6411b..834efd07eb32 100644 --- a/src/pages/tenant/conditional/list-named-locations/index.js +++ b/src/pages/tenant/conditional/list-named-locations/index.js @@ -2,6 +2,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline"; +import { LocationOn } from "@mui/icons-material"; const Page = () => { const pageTitle = "Named Locations"; @@ -11,51 +13,58 @@ const Page = () => { label: "Add location to named location", type: "GET", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", change: "addLocation", }, fields: [{ type: "textField", name: "input", label: "Country Code" }], confirmText: "Enter a two-letter country code, e.g., US.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation", }, { label: "Remove location from named location", type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", change: "removeLocation", }, fields: [{ type: "textField", name: "input", label: "Country Code" }], confirmText: "Enter a two-letter country code, e.g., US.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation", }, { label: "Add IP to named location", type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", change: "addIp", }, fields: [{ type: "textField", name: "input", label: "IP" }], confirmText: "Enter an IP in CIDR format, e.g., 1.1.1.1/32.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation", }, { label: "Remove IP from named location", type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", change: "removeIp", }, fields: [{ type: "textField", name: "input", label: "IP" }], - confirmText: "Enter an IP in CIDR format, e.g., 1.1.1.1/32.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation", }, ]; const offCanvas = { - extendedInfoFields: ["displayName", "type", "rangeOrLocation"], + extendedInfoFields: ["displayName", "rangeOrLocation"], actions: actions, }; @@ -67,7 +76,7 @@ const Page = () => { offCanvas={offCanvas} cardButton={ <> - @@ -75,7 +84,7 @@ const Page = () => { simpleColumns={[ "displayName", "includeUnknownCountriesAndRegions", - "type", + "isTrusted", "rangeOrLocation", "modifiedDateTime", ]} diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index 35f77a73f286..df42715f9801 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -5,6 +5,7 @@ import { Check as CheckIcon, Delete as DeleteIcon, MenuBook as MenuBookIcon, + AddModerator as AddModeratorIcon, Visibility as VisibilityIcon, } from "@mui/icons-material"; import { Button } from "@mui/material"; @@ -105,7 +106,11 @@ const Page = () => { - diff --git a/src/pages/tenant/conditional/list-template/index.js b/src/pages/tenant/conditional/list-template/index.js index 41a8010d6b50..8db07cbdca14 100644 --- a/src/pages/tenant/conditional/list-template/index.js +++ b/src/pages/tenant/conditional/list-template/index.js @@ -2,15 +2,65 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { Delete, GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Available Conditional Access Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "GET", url: "/api/RemoveCATemplate", + icon: , data: { ID: "GUID" }, confirmText: "Do you want to delete the template?", multiPost: false, diff --git a/src/pages/tenant/gdap-management/invites/add.js b/src/pages/tenant/gdap-management/invites/add.js index 22d72810f0c8..b470cde23584 100644 --- a/src/pages/tenant/gdap-management/invites/add.js +++ b/src/pages/tenant/gdap-management/invites/add.js @@ -40,12 +40,12 @@ const Page = () => { const createCippDefaults = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["ListGDAPRoleTemplatesAutocomplete"], + relatedQueryKeys: ["ListGDAPRoleTemplatesAutocomplete", "ListGDAPRoleTemplates"], }); const templateList = ApiGetCall({ url: "/api/ExecGDAPRoleTemplate", - queryKey: "ListGDAPRoleTemplatesAutocomplete", + queryKey: "ListGDAPRoleTemplates", }); const selectedTemplate = useWatch({ control: formControl.control, name: "roleMappings" }); diff --git a/src/pages/tenant/gdap-management/invites/index.js b/src/pages/tenant/gdap-management/invites/index.js index 7cf52a876857..54670b22330e 100644 --- a/src/pages/tenant/gdap-management/invites/index.js +++ b/src/pages/tenant/gdap-management/invites/index.js @@ -3,6 +3,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Add } from "@mui/icons-material"; import Link from "next/link"; const pageTitle = "GDAP Invites"; @@ -13,7 +14,7 @@ const Page = () => { return ( + } diff --git a/src/pages/tenant/gdap-management/offboarding.js b/src/pages/tenant/gdap-management/offboarding.js index 1fb872a6d2da..64db45f06b4f 100644 --- a/src/pages/tenant/gdap-management/offboarding.js +++ b/src/pages/tenant/gdap-management/offboarding.js @@ -7,7 +7,7 @@ import { CippFormComponent } from "/src/components/CippComponents/CippFormCompon import vendorTenantList from "/src/data/vendorTenantList"; import { Box, Grid, Stack } from "@mui/system"; import { Alert, Divider, Typography } from "@mui/material"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall, ApiGetCallWithPagination } from "/src/api/ApiCall"; import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; import { ShieldCheckIcon } from "@heroicons/react/24/outline"; import { Apps, Description, Widgets } from "@mui/icons-material"; @@ -60,7 +60,7 @@ const Page = () => { queryKey: "ListMSPApps-" + tenantId?.value, }); - const vendorApps = ApiGetCall({ + const vendorApps = ApiGetCallWithPagination({ url: "/api/ListGraphRequest", data: { Endpoint: "servicePrincipals", @@ -81,6 +81,7 @@ const Page = () => { hideBackButton={true} hidePageType={true} postUrl="/api/ExecOffboardTenant" + resetForm={true} > @@ -193,7 +194,7 @@ const Page = () => { }, valueField: "appId", }} - disabled={vendorApps?.data?.Results?.length > 0 ? false : true} + disabled={vendorApps?.data?.pages?.[0]?.Results?.length > 0 ? false : true} /> { tenantInTitle={false} queryKey="ListTenantOnboarding" cardButton={ - } diff --git a/src/pages/tenant/gdap-management/role-templates/index.js b/src/pages/tenant/gdap-management/role-templates/index.js index def2717cc986..3b82189aeb14 100644 --- a/src/pages/tenant/gdap-management/role-templates/index.js +++ b/src/pages/tenant/gdap-management/role-templates/index.js @@ -9,7 +9,7 @@ import { useEffect, useState } from "react"; import { Box, Stack } from "@mui/system"; import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; -import { Edit } from "@mui/icons-material"; +import { Edit, AddBox } from "@mui/icons-material"; const Page = () => { const pageTitle = "GDAP Role Templates"; @@ -42,7 +42,6 @@ const Page = () => { urlFromData: true, relatedQueryKeys: "ListGDAPRoleTemplates", }); - useEffect(() => { if (currentTemplates.isSuccess) { @@ -98,7 +97,11 @@ const Page = () => { tenantInTitle={false} sx={{ flexGrow: 1, pb: 4 }} cardButton={ - } diff --git a/src/pages/tenant/gdap-management/roles/index.js b/src/pages/tenant/gdap-management/roles/index.js index 2de5ad32b096..e97820abaf08 100644 --- a/src/pages/tenant/gdap-management/roles/index.js +++ b/src/pages/tenant/gdap-management/roles/index.js @@ -3,15 +3,15 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { AdminPanelSettings, Add, Delete } from "@mui/icons-material"; import Link from "next/link"; -import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; const pageTitle = "GDAP Role Mappings"; const actions = [ { label: "Add to Template", - icon: , + icon: , type: "POST", url: "/api/ExecGDAPRoleTemplate?Action=Add", confirmText: "Select a template to add the selected role mapping(s) to.", @@ -40,7 +40,7 @@ const actions = [ }, { label: "Delete Mapping", - icon: , + icon: , type: "POST", url: "/api/ExecDeleteGDAPRoleMapping", data: { @@ -64,7 +64,11 @@ const Page = () => { simpleColumns={simpleColumns} tenantInTitle={false} cardButton={ - } diff --git a/src/pages/tenant/administration/application-consent/index.js b/src/pages/tenant/reports/application-consent/index.js similarity index 100% rename from src/pages/tenant/administration/application-consent/index.js rename to src/pages/tenant/reports/application-consent/index.js diff --git a/src/pages/tenant/administration/list-csp-licenses/index.jsx b/src/pages/tenant/reports/list-csp-licenses/index.jsx similarity index 94% rename from src/pages/tenant/administration/list-csp-licenses/index.jsx rename to src/pages/tenant/reports/list-csp-licenses/index.jsx index 611d4f5d9b10..9e6264df4c74 100644 --- a/src/pages/tenant/administration/list-csp-licenses/index.jsx +++ b/src/pages/tenant/reports/list-csp-licenses/index.jsx @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline"; -import { DeleteForever } from "@mui/icons-material"; +import { DeleteForever, ShoppingCart } from "@mui/icons-material"; import { Button } from "@mui/material"; import Link from "next/link"; @@ -93,7 +93,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/tenant/administration/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js similarity index 58% rename from src/pages/tenant/administration/list-licenses/index.js rename to src/pages/tenant/reports/list-licenses/index.js index 1a4abfc73707..417e1ef16910 100644 --- a/src/pages/tenant/administration/list-licenses/index.js +++ b/src/pages/tenant/reports/list-licenses/index.js @@ -5,28 +5,16 @@ const Page = () => { const pageTitle = "Licenses Report"; const apiUrl = "/api/ListLicenses"; - const actions = []; // No actions specified, setting to empty array - - const offCanvas = null; // No off-canvas details provided - const simpleColumns = [ "Tenant", "License", "CountUsed", "CountAvailable", "TotalLicenses", - "TermInfo", + "TermInfo", // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby ]; - return ( - - ); + return ; }; Page.getLayout = (page) => {page}; diff --git a/src/pages/tenant/standards/bpa-report/index.js b/src/pages/tenant/standards/bpa-report/index.js index 2c786341cd68..d80e5eb5ae1c 100644 --- a/src/pages/tenant/standards/bpa-report/index.js +++ b/src/pages/tenant/standards/bpa-report/index.js @@ -3,10 +3,15 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import Link from "next/link"; import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete, Edit } from "@mui/icons-material"; +import { CopyAll, Delete, Edit, AddBox, GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Best Practice Reports"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const actions = [ { label: "View Report", @@ -30,6 +35,50 @@ const Page = () => { color: "success", target: "_self", }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "GET", @@ -48,13 +97,13 @@ const Page = () => { title={pageTitle} apiUrl="/api/listBPATemplates" cardButton={ - } actions={actions} simpleColumns={["Name", "Style"]} - queryKey="ListPATemplates" + queryKey="ListBPATemplates" /> ); }; diff --git a/src/pages/tenant/standards/bpa-report/view.js b/src/pages/tenant/standards/bpa-report/view.js index 2760fdc0a446..5720f5d6f049 100644 --- a/src/pages/tenant/standards/bpa-report/view.js +++ b/src/pages/tenant/standards/bpa-report/view.js @@ -28,7 +28,7 @@ const Page = () => { const [layoutMode, setLayoutMode] = useState("Table"); const bpaTemplateList = ApiGetCall({ url: "/api/listBPATemplates", - queryKey: "ListBPATemplates", + queryKey: "ListBPATemplates-All", }); const tenantFilter = useSettings().currentTenant; const bpaData = ApiGetCall({ diff --git a/src/pages/tenant/standards/domains-analyser/index.js b/src/pages/tenant/standards/domains-analyser/index.js index 218e8a8633b4..c90254aab4df 100644 --- a/src/pages/tenant/standards/domains-analyser/index.js +++ b/src/pages/tenant/standards/domains-analyser/index.js @@ -6,7 +6,7 @@ import { ApiGetCall } from "../../../../api/ApiCall"; import { useSettings } from "../../../../hooks/use-settings"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; import { CippDomainCards } from "../../../../components/CippCards/CippDomainCards"; -import { DeleteForever } from "@mui/icons-material"; +import { DeleteForever, TravelExplore, Refresh } from "@mui/icons-material"; const Page = () => { const currentTenant = useSettings().currentTenant; @@ -36,11 +36,17 @@ const Page = () => { apiUrl="/api/ListDomainAnalyser" cardButton={ <> - {/* This needs to be replaced with a CippApiDialog. */} - + } prependComponents={} diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index e041ea9cd23b..6dcc15daeb7f 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -2,22 +2,24 @@ import { Alert, Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import Link from "next/link"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete, PlayArrow } from "@mui/icons-material"; +import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub } from "@mui/icons-material"; import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; import { Grid } from "@mui/system"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; const Page = () => { const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" }); - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); const pageTitle = "Standard Templates"; const actions = [ { label: "Edit Template", //when using a link it must always be the full path /identity/administration/users/[id] for example. link: "/tenant/standards/template?id=[GUID]", - icon: , + icon: , color: "success", target: "_self", }, @@ -51,6 +53,50 @@ const Page = () => { confirmText: "Are you sure you want to force a run of this template?", multiPost: false, }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "POST", @@ -105,7 +151,7 @@ const Page = () => { apiUrl="/api/listStandardTemplates" tenantInTitle={false} cardButton={ - } diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index 2a5934014bc6..959530865f0e 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -74,7 +74,10 @@ const Page = () => { }, {}); const handleOpenDialog = () => setDialogOpen(true); - const handleCloseDialog = () => setDialogOpen(false); + const handleCloseDialog = () => { + setDialogOpen(false); + setSearchQuery(""); + }; const filterStandards = (standardsList) => standardsList.filter( @@ -98,7 +101,6 @@ const Page = () => { if (match) { standardName = match[1]; } - console.log("Adding multiple", standardName); setSelectedStandards((prev) => { const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName)); diff --git a/src/pages/tenant/administration/appapproval/index.js b/src/pages/tenant/tools/appapproval/index.js similarity index 100% rename from src/pages/tenant/administration/appapproval/index.js rename to src/pages/tenant/tools/appapproval/index.js diff --git a/src/pages/tenant/tools/geoiplookup/index.js b/src/pages/tenant/tools/geoiplookup/index.js index c1d68fa0b041..df8c10702556 100644 --- a/src/pages/tenant/tools/geoiplookup/index.js +++ b/src/pages/tenant/tools/geoiplookup/index.js @@ -3,7 +3,7 @@ import { Grid, Stack } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; -import { Delete, Search } from "@mui/icons-material"; +import { Add, Delete, Search } from "@mui/icons-material"; import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; import { ApiPostCall } from "../../../../api/ApiCall"; import { getCippValidator } from "../../../../utils/get-cipp-validator"; @@ -26,15 +26,33 @@ const Page = () => { customFunction: (row) => setIpAddress(row.RowKey), noConfirm: true, icon: , + hideBulk: true, + }, + { + label: "Add to Whitelist", + url: `/api/ExecAddTrustedIP`, + type: "POST", + data: { + IP: "RowKey", + State: "!Trusted", + }, + icon: , + confirmText: "Are you sure you want to add this IP to the whitelist?", + multiPost: false, + condition: (row) => row.state !== "Trusted", }, { label: "Remove from Whitelist", - customFunction: (row) => - addGeoIP.mutate({ - url: `/api/ExecAddTrustedIP?IP=${row.RowKey}&TenantFilter=${currentTenant}&State=NotTrusted`, - }), + url: `/api/ExecAddTrustedIP`, + type: "POST", + data: { + IP: "RowKey", + State: "!NotTrusted", + }, icon: , confirmText: "Are you sure you want to remove this IP from the whitelist?", + multiPost: false, + condition: (row) => row.state !== "NotTrusted", }, ]; @@ -44,13 +62,23 @@ const Page = () => { const handleAddToWhitelist = () => { addGeoIP.mutate({ - url: `/api/ExecAddTrustedIP?IP=${ip}&TenantFilter=${currentTenant}&State=Trusted`, + url: `/api/ExecAddTrustedIP`, + data: { + IP: ip, + State: "Trusted", + tenantFilter: currentTenant, + } }); }; const handleRemoveFromWhitelist = () => { addGeoIP.mutate({ - url: `/api/ExecAddTrustedIP?IP=${ip}&TenantFilter=${currentTenant}&State=NotTrusted`, + url: `/api/ExecAddTrustedIP`, + data: { + IP: ip, + State: "NotTrusted", + tenantFilter: currentTenant, + } }); }; diff --git a/src/pages/tenant/administration/graph-explorer/index.js b/src/pages/tenant/tools/graph-explorer/index.js similarity index 100% rename from src/pages/tenant/administration/graph-explorer/index.js rename to src/pages/tenant/tools/graph-explorer/index.js diff --git a/src/pages/tenant/standards/individual-domains/index.js b/src/pages/tenant/tools/individual-domains/index.js similarity index 100% rename from src/pages/tenant/standards/individual-domains/index.js rename to src/pages/tenant/tools/individual-domains/index.js diff --git a/src/pages/tools/community-repos/index.js b/src/pages/tools/community-repos/index.js new file mode 100644 index 000000000000..6fb4a0137fa6 --- /dev/null +++ b/src/pages/tools/community-repos/index.js @@ -0,0 +1,470 @@ +import { useState } from "react"; +import { Layout as DashboardLayout } from "/src/layouts"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Stack, + Card, + CardContent, + Divider, + Skeleton, + IconButton, + Tooltip, + Typography, + Alert, + Chip, + SvgIcon, +} from "@mui/material"; +import { MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useForm, FormProvider } from "react-hook-form"; +import { Radio, RadioGroup, FormControlLabel } from "@mui/material"; +import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import AddIcon from "@mui/icons-material/Add"; +import { Box } from "@mui/system"; +import { Add, AddBox, Close, ForkLeft, OpenInNew } from "@mui/icons-material"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; +import { ApiGetCall } from "../../../api/ApiCall"; + +const Page = () => { + const [openSearch, setOpenSearch] = useState(false); + const [results, setResults] = useState([]); + const [repo, setRepo] = useState(""); + const [user, setUser] = useState(""); + const [org, setOrg] = useState(""); + const [openCreate, setOpenCreate] = useState(false); + const createForm = useForm({ mode: "onChange", defaultValues: { Type: "user" } }); + + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + }); + + const createMutation = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["CommunityRepos"], + }); + + const handleCreateRepo = (values) => { + createMutation.mutate({ + url: "/api/ExecGitHubAction", + data: { + Action: "CreateRepo", + Type: values.type, + Name: values.repoName, + Org: values.orgName?.value, + Description: values.Description, + Private: values.Private, + }, + }); + }; + + const actions = [ + { + label: "View Templates", + link: "/tools/community-repos/repo?name=[FullName]&branch=[DefaultBranch]", + icon: , + }, + { + label: "Delete", + type: "POST", + url: "/api/ExecCommunityRepo", + data: { Action: "Delete", Id: "Id" }, + confirmText: "Are you sure you want to delete this repo?", + icon: , + multiPost: false, + queryKey: "CommunityRepos", + }, + { + label: "Set Upload Branch", + type: "POST", + url: "/api/ExecCommunityRepo", + data: { Action: "SetBranch", Id: "Id" }, + icon: , + fields: [ + { + type: "select", + name: "Branch", + label: "Branch", + api: { + url: "/api/ExecGitHubAction", + type: "GET", + data: { + Action: "GetBranches", + FullName: "FullName", + }, + dataKey: "Results", + labelField: "name", + valueField: "name", + processFieldData: true, + }, + }, + ], + hideBulk: true, + confirmText: "Are you sure you want to set the branch for this repository?", + condition: (row) => row.WriteAccess === true, + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "Owner", + "Name", + "Description", + "URL", + "Visibility", + "DefaultBranch", + "UploadBranch", + "Permissions", + ], + actions: actions, + }; + + const searchMutation = ApiPostCall({ + urlFromData: true, + onResult: (resp) => { + setResults(resp?.Results || []); + }, + }); + + const searchForm = useForm({ defaultValues: { searchType: "user", searchTerm: [] } }); + const watchSearchTerm = searchForm.watch("searchTerm"); + + const handleSearch = () => { + const searchTerms = watchSearchTerm.map((t) => t.value) ?? []; + searchMutation.mutate({ + url: "/api/ExecGitHubAction", + data: { + Action: "Search", + Repository: repo ? repo : "", + User: user ? user : "", + Org: org ? org : "", + SearchTerm: searchTerms, + Type: "repositories", + }, + }); + }; + + const addMutation = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["CommunityRepos"], + }); + + const handleAdd = (repoId) => { + addMutation.mutate({ + url: "/api/ExecCommunityRepo", + data: { Action: "Add", Id: repoId }, + }); + }; + + return ( + <> + + + + + } + /> + setOpenCreate(false)}> + Create New Repository + + + { + createForm.setValue("Type", e.target.value); + }} + > + } label="User" /> + } label="Org" /> + + + + + + + + + + + + + + + + + + setOpenSearch(false)}> + Add Community Repositories from GitHub + + + searchForm.setValue("searchType", e.target.value)} + > + } label="User" /> + } label="Org" /> + } label="Repository" /> + + + + setRepo(e.target.value)} + required={true} + /> + + + setUser(e.target.value)} + required={true} + /> + + + + + setOrg(e.target.value)} + required={true} + /> + + + + + + {searchMutation.isPending || + (searchMutation.isSuccess && ( + + + Search Results + + ))} + {searchMutation.isPending ? ( + <> + + + Searching... + + + + + + + + + + + + ) : ( + <> + {(searchMutation.isSuccess && results.length === 0) || + (searchMutation.isError && ( + + No search results found. Refine your query and try again. + + ))} + + {results.map((r) => ( + + + + + handleAdd(r.id)}> + + + + + window.open(r.html_url, "_blank")} + > + + + + + + + {r.full_name} + + + + + {r.html_url} + + + + + + ))} + + + )} + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/tools/community-repos/repo.js b/src/pages/tools/community-repos/repo.js new file mode 100644 index 000000000000..8b93c7dae27e --- /dev/null +++ b/src/pages/tools/community-repos/repo.js @@ -0,0 +1,250 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useState, useEffect } from "react"; +import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall"; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Skeleton, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import CippJSONView from "/src/components/CippFormPages/CippJSONView"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { CippAutoComplete } from "/src/components/CippComponents/CippAutocomplete"; +import React from "react"; +import { CloudDownload } from "@mui/icons-material"; + +const Page = () => { + const router = useRouter(); + const { name, branch } = router.query; + const [openJsonDialog, setOpenJsonDialog] = useState(false); + const [fileResults, setFileResults] = useState([]); + const [jsonContent, setJsonContent] = useState({}); + const [branches, setBranches] = useState([]); + const [selectedBranch, setSelectedBranch] = useState(branch); + const [selectedRepo, setSelectedRepo] = useState(name); + + const fileQuery = ApiPostCall({ + onResult: (resp) => { + let content = resp?.Results?.content?.trim() || "{}"; + // remove non-printable characters from the beginning and end + content = content.replace( + /^[\u0000-\u001F\u007F-\u009F]+|[\u0000-\u001F\u007F-\u009F]+$/g, + "" + ); + try { + setJsonContent(JSON.parse(content)); + } catch (e) { + console.error("Invalid JSON content:", e); + setJsonContent({}); + } + }, + }); + + const branchQuery = ApiGetCall({ + url: "/api/ExecGitHubAction", + data: { + Action: "GetBranches", + FullName: selectedRepo, + }, + onResult: (resp) => { + const branchList = resp?.Results || []; + setBranches(branchList); + if (branchList.length === 1) { + setSelectedBranch(branchList[0].name); + fetchFileTree(branchList[0].name); + } + const mainBranch = branchList.find((branch) => ["main", "master"].includes(branch.name)); + if (mainBranch) { + setSelectedBranch(mainBranch.name); + fetchFileTree(mainBranch.name); + } + }, + queryKey: `${selectedRepo}-branches`, + waiting: selectedRepo !== "", + }); + + const fileTreeQuery = ApiGetCall({ + url: "/api/ExecGitHubAction", + data: { + Action: "GetFileTree", + FullName: selectedRepo, + Branch: selectedBranch, + }, + onResult: (resp) => { + setFileResults(resp?.Results || []); + }, + queryKey: `${selectedRepo}-${selectedBranch}-filetree`, + waiting: selectedRepo !== "" && selectedBranch !== "", + }); + + const fetchFileTree = (branch) => { + if (selectedRepo !== "" && branch !== "") { + if (!fileTreeQuery.waiting) { + fileTreeQuery.waiting = true; + } + fileTreeQuery.refetch(); + } + }; + + const handleJsonView = (path) => { + fileQuery.mutate({ + url: "/api/ExecGitHubAction", + data: { + Action: "GetFileContents", + FullName: selectedRepo, + Path: path, + Branch: branch, + }, + }); + setOpenJsonDialog(true); + }; + + useEffect(() => { + if (selectedRepo) { + branchQuery.refetch(); + } + }, [selectedRepo]); + + useEffect(() => { + if (selectedBranch) { + fetchFileTree(selectedBranch); + } + }, [selectedBranch]); + + const updateQueryParams = (prop, newValue) => { + const query = { ...router.query }; + if (query[prop] !== newValue) { + query[prop] = newValue; + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + } + }; + + const MemoizedCippAutoComplete = React.memo((props) => { + return ; + }); + + return ( + <> + + + { + if (newValue.value === selectedRepo) return; + setSelectedRepo(newValue.value); + updateQueryParams(newValue.value); + }} + api={{ + url: "/api/ListCommunityRepos", + queryKey: "CommunityRepos", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }} + multiple={false} + label="Select Repository" + placeholder="Select Repository" + disableClearable + /> + + + { + if (newValue.value === selectedBranch) return; + setSelectedBranch(newValue.value); + updateQueryParams("branch", newValue.value); + }} + options={branches.map((branch) => ({ label: branch.name, value: branch.name }))} + multiple={false} + label="Select Branch" + placeholder="Select Branch" + disableClearable + isFetching={branchQuery.isPending} + /> + + + } + data={fileResults} + apiDataKey="Results" + queryKey="JsonTemplates" + simpleColumns={["path", "html_url"]} + actions={[ + { + label: "View Template", + customFunction: (row) => handleJsonView(row.path), + noConfirm: true, + icon: , + hideBulk: true, + }, + { + label: "Import Template", + url: "/api/ExecCommunityRepo", + icon: , + type: "POST", + data: { + Action: "ImportTemplate", + FullName: selectedRepo, + Path: "path", + Branch: selectedBranch, + }, + confirmText: "Are you sure you want to import [path]?", + }, + ]} + isFetching={fileTreeQuery.isFetching} + refreshFunction={() => fetchFileTree(selectedBranch)} + /> + setOpenJsonDialog(false)} + > + Template Details + + {fileQuery.isPending ? ( + + + + ) : ( + + )} + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/tools/templatelib/index.jsx b/src/pages/tools/templatelib/index.jsx index 18349fab9b8d..5fa63b3bbf9b 100644 --- a/src/pages/tools/templatelib/index.jsx +++ b/src/pages/tools/templatelib/index.jsx @@ -1,11 +1,15 @@ -import React from "react"; -import { Grid, Divider, Typography, CircularProgress, Alert } from "@mui/material"; -import { useForm } from "react-hook-form"; +import React, { useEffect } from "react"; +import { Grid, Divider, Typography, CircularProgress, Alert, Chip, Link } from "@mui/material"; +import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; import { CippFormTenantSelector } from "../../../components/CippComponents/CippFormTenantSelector"; +import { Box } from "@mui/system"; +import { CippFormCondition } from "../../../components/CippComponents/CippFormCondition"; +import { ApiGetCall } from "/src/api/ApiCall"; +import NextLink from "next/link"; const TemplateLibrary = () => { const currentTenant = useSettings().currentTenant; @@ -20,81 +24,210 @@ const TemplateLibrary = () => { }, }); + const templateRepo = useWatch({ control: formControl.control, name: "templateRepo" }); + const customDataFormatter = (values) => { const startDate = new Date(); startDate.setHours(0, 0, 0, 0); const unixTime = Math.floor(startDate.getTime() / 1000) - 45; return { - TenantFilter: values.tenantFilter.value, - Name: `CIPP Template ${values.tenantFilter.value}`, + TenantFilter: values?.tenantFilter?.value ? values?.tenantFilter?.value : "No tenant", + Name: `CIPP Template ${ + values.tenantFilter?.value ? values.tenantFilter?.value : values.templateRepo?.value + }`, Command: { value: `New-CIPPTemplateRun` }, Parameters: { TemplateSettings: { ...values } }, ScheduledTime: unixTime, - Recurrence: { value: "4h" }, + Recurrence: { value: values.tenantFilter?.value ? "4h" : "7d" }, }; }; + useEffect(() => { + if (templateRepo?.value) { + formControl.setValue("templateRepoBranch", { + label: templateRepo.addedFields.branch, + value: templateRepo.addedFields.branch, + }); + } + }, [templateRepo?.value]); + return ( - + Template libraries are tenants set up to retrieve the latest version of a specific tenants policies. These are then stored in CIPPs templates, allowing you to keep an up to date copy of the policies.This copy occurs every 4 hours. + + There are also template repositories, these are community driven and are used to share + templates with other users. Template repositories are downloaded when new versions are + released. + Enabling this feature will overwrite templates with the same name. - - - - - - - - - - Conditional Access - + + + + + + + + + + + + + + + `${option.Name} (${option.URL})`, + addedField: { + branch: "DefaultBranch", + }, + }} + formControl={formControl} + multiple={false} + /> + + + + {templateRepo?.value && ( + + + Repository Branch + + + + )} + + + + Conditional Access + + + - - Intune - - - - + + + Intune + + + + + + + + + + Template Repository files + + + + + + + ); diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 47c64308f980..6c06e2ca3a80 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -3,12 +3,24 @@ import Head from "next/head"; import { CippImageCard } from "../components/CippCards/CippImageCard"; import { Layout as DashboardLayout } from "../layouts/index.js"; import { ApiGetCall } from "../api/ApiCall"; +import { useState, useEffect } from "react"; const Page = () => { const orgData = ApiGetCall({ url: "/.auth/me", queryKey: "me", }); + const blockedRoles = ["anonymous", "authenticated"]; + const [userRoles, setUserRoles] = useState([]); + + useEffect(() => { + if (orgData.isSuccess) { + const roles = orgData.data?.clientPrincipal?.userRoles.filter( + (role) => !blockedRoles.includes(role) + ); + setUserRoles(roles ?? []); + } + }, [orgData, blockedRoles]); return ( <> @@ -32,14 +44,16 @@ const Page = () => { sx={{ height: "100%" }} // Ensure the container takes full height > - + {orgData.isSuccess && Array.isArray(userRoles) && ( + 0 ? "Return" : "Login"} + link={userRoles.length > 0 ? "/" : "/.auth/login/aad"} + /> + )} diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 73c65b8dbdad..3d6841d267b4 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -219,6 +219,10 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { } } + if (cellName === "ClientId") { + return isText ? data : ; + } + if (cellName === "excludedTenants") { //check if data is an array. if (Array.isArray(data)) { @@ -266,6 +270,19 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { return isText ? data : ; } + if (cellName === "Parameters.ScheduledBackupValues") { + return isText ? ( + JSON.stringify(data) + ) : ( + { + return { key, value: data[key] }; + })} + tableTitle={getCippTranslation(cellName)} + /> + ); + } + // Handle null or undefined data if (data === null || data === undefined) { return isText ? ( @@ -425,6 +442,23 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ); } + if (cellName === "Visibility") { + const gitHubVisibility = ["public", "private", "internal"]; + if (gitHubVisibility.includes(data)) { + return isText ? ( + data + ) : ( + + ); + } + } + if (cellName === "AutoMapUrl") { return isText ? data : ; } diff --git a/src/utils/get-cipp-license-translation.js b/src/utils/get-cipp-license-translation.js index 813e52a5d1e9..e77574c713a9 100644 --- a/src/utils/get-cipp-license-translation.js +++ b/src/utils/get-cipp-license-translation.js @@ -21,5 +21,9 @@ export const getCippLicenseTranslation = (licenseArray) => { } }); - return licenses.join(", "); + const result = licenses.join(", "); + if (!result) { + return "No Licenses Assigned"; + } + return result; };