diff --git a/package.json b/package.json index 45e4b83f482d..77ed24334c20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.5.0", + "version": "10.5.1", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { @@ -39,7 +39,7 @@ "@musement/iso-duration": "^1.0.0", "@nivo/core": "^0.99.0", "@nivo/sankey": "^0.99.0", - "@react-pdf/renderer": "^4.3.2", + "@react-pdf/renderer": "^4.5.1", "@reduxjs/toolkit": "^2.11.2", "@tanstack/query-sync-storage-persister": "^5.90.25", "@tanstack/react-query": "^5.100.10", @@ -47,14 +47,14 @@ "@tanstack/react-query-persist-client": "^5.96.2", "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^3.22.3", - "@tiptap/extension-heading": "^3.4.1", + "@tiptap/extension-heading": "^3.22.3", "@tiptap/extension-table": "^3.20.5", "@tiptap/pm": "^3.22.3", "@tiptap/react": "^3.20.5", "@tiptap/starter-kit": "^3.20.5", "@vvo/tzdb": "^6.198.0", "apexcharts": "5.10.4", - "axios": "1.15.0", + "axios": "1.16.1", "date-fns": "4.1.0", "diff": "^8.0.3", "dompurify": "^3.4.3", @@ -84,7 +84,7 @@ "react-dom": "19.2.6", "react-dropzone": "15.0.0", "react-error-boundary": "^6.1.1", - "react-hook-form": "^7.72.0", + "react-hook-form": "^7.76.1", "react-hot-toast": "2.6.0", "react-html-parser": "^2.0.2", "react-leaflet": "5.0.0", @@ -96,7 +96,7 @@ "react-redux": "9.2.0", "react-syntax-highlighter": "^16.1.0", "react-time-ago": "^7.3.3", - "react-virtuoso": "^4.18.5", + "react-virtuoso": "^4.18.7", "recharts": "^3.8.1", "redux": "5.0.1", "redux-persist": "^6.0.0", diff --git a/public/version.json b/public/version.json index b7e5a36d3a96..57f16cb95ddd 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.5.0" + "version": "10.5.1" } diff --git a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx index 7326ae56df0c..1d5add90b7ee 100644 --- a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx +++ b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx @@ -99,7 +99,9 @@ export const ForcedSsoMigrationDialog = () => { 'SSO migration failed. Please try again.'} - If this error persists, contact your CIPP administrator. + The app registration may have been created already — clicking Try Again{' '} + will pick up where it left off rather than starting over. If the error persists, + contact your CIPP administrator. ) : null} diff --git a/src/components/CippComponents/SsoMigrationDialog.jsx b/src/components/CippComponents/SsoMigrationDialog.jsx index 03026daec36a..cb4d9691b187 100644 --- a/src/components/CippComponents/SsoMigrationDialog.jsx +++ b/src/components/CippComponents/SsoMigrationDialog.jsx @@ -60,7 +60,8 @@ export const SsoMigrationDialog = ({ meData }) => { const result = ssoSetup.data?.data?.Results ?? ssoSetup.data?.Results const isSuccess = result?.severity === 'success' - const isError = ssoSetup.isError || result?.severity === 'failed' + const isPartial = result?.severity === 'warning' && result?.canRepair + const isError = ssoSetup.isError || result?.severity === 'failed' || (result?.severity === 'warning' && !result?.canRepair) return ( @@ -109,6 +110,20 @@ export const SsoMigrationDialog = ({ meData }) => { {result.message} + ) : isPartial ? ( + + + App created — secret creation failed + + + The CIPP-SSO app registration ({result.appId}) was created successfully, but the + client secret could not be generated. The app ID is saved. + + + Open Advanced → Super Admin → SSO and click{' '} + Repair to finish setup. + + ) : isError ? ( {result?.message || ssoSetup.error?.message || 'SSO setup failed. It will be retried automatically.'} diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index ad578556a517..9a3559190372 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -824,7 +824,7 @@ const CippAddEditUser = (props) => { label: group.displayName, value: group.id, addedFields: { - groupType: group.calculatedGroupType || group.groupType, + groupType: group.groupType, }, })) || [] } diff --git a/src/components/CippSettings/CippSSOSettings.jsx b/src/components/CippSettings/CippSSOSettings.jsx index e22cd1ab6edf..5499b4e694d1 100644 --- a/src/components/CippSettings/CippSSOSettings.jsx +++ b/src/components/CippSettings/CippSSOSettings.jsx @@ -1,10 +1,9 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { Alert, Button, CardActions, CardContent, - CardHeader, Chip, Divider, Skeleton, @@ -20,16 +19,16 @@ import { CippApiResults } from "../CippComponents/CippApiResults"; const statusLabels = { none: { label: "Not Configured", color: "default" }, - app_created: { label: "App Created", color: "info" }, - appid_stored: { label: "App ID Stored", color: "info" }, + app_created: { label: "App Created — Secret Pending", color: "warning" }, + appid_stored: { label: "App ID Stored — Secret Pending", color: "warning" }, secrets_stored: { label: "Secrets Stored", color: "success" }, complete: { label: "Complete", color: "success" }, error: { label: "Error", color: "error" }, }; -export const CippSSOSettings = () => { - const [showCreate, setShowCreate] = useState(false); +const repairableStatuses = new Set(["error", "app_created", "appid_stored"]); +export const CippSSOSettings = () => { const formControl = useForm({ mode: "onChange", defaultValues: { multiTenant: false }, @@ -49,32 +48,82 @@ export const CippSSOSettings = () => { if (ssoStatus.isSuccess && ssoStatus.data?.Results) { const data = ssoStatus.data.Results; formControl.reset({ multiTenant: data.multiTenant ?? false }); - setShowCreate(!data.configured); } }, [ssoStatus.isSuccess, ssoStatus.data]); - const handleUpdate = () => { + const data = ssoStatus.data?.Results; + const statusKey = data?.status ?? "none"; + const statusInfo = statusLabels[statusKey] ?? statusLabels.none; + const hasAppId = Boolean(data?.appId); + // Server-provided canRepair is authoritative when present; fall back to local heuristic. + const canRepair = + data?.canRepair ?? + (hasAppId && repairableStatuses.has(statusKey)); + const isProvisioned = + statusKey === "complete" || (statusKey === "secrets_stored" && hasAppId); + // Show "Create SSO App" whenever there isn't a working app AND there's nothing to repair — + // covers fresh installs AND legacy broken installs where the AppId was never persisted + // (the original "Failed to create client secret after 5 attempts" bug). + const showCreate = !isProvisioned && !canRepair; + const isOrphanedError = statusKey === "error" && !hasAppId; + + const handleCreate = () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { + Action: "Create", + multiTenant: formControl.getValues("multiTenant"), + }, + }); + }; + + const handleRepair = () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { Action: "Repair" }, + }); + }; + + const handleRecreate = () => { if ( !window.confirm( - "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?" + "Recreate will clear the current SSO record and provision a brand new CIPP-SSO app. The previous app registration will be left in your Entra tenant (you can delete it manually). Continue?" ) ) { return; } - ssoAction.mutate({ - url: "/api/ExecSSOSetup", - data: { - Action: "Update", - multiTenant: formControl.getValues("multiTenant"), + // Clear first, then create. ApiPostCall chains via the success refetch — call sequentially. + ssoAction.mutate( + { + url: "/api/ExecSSOSetup", + data: { Action: "Recreate" }, }, - }); + { + onSuccess: () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { + Action: "Create", + multiTenant: formControl.getValues("multiTenant"), + }, + }); + }, + } + ); }; - const handleCreate = () => { + const handleUpdate = () => { + if ( + !window.confirm( + "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?" + ) + ) { + return; + } ssoAction.mutate({ url: "/api/ExecSSOSetup", data: { - Action: "Create", + Action: "Update", multiTenant: formControl.getValues("multiTenant"), }, }); @@ -87,9 +136,6 @@ export const CippSSOSettings = () => { }); }; - const data = ssoStatus.data?.Results; - const statusInfo = statusLabels[data?.status] ?? statusLabels.none; - return ( @@ -141,13 +187,38 @@ export const CippSSOSettings = () => { )} {data?.lastError && ( - <> - - - {data.lastError} - - - + + + + {canRepair + ? "Setup did not finish" + : isOrphanedError + ? "Previous setup failed" + : "Error"} + + {data.lastError} + {canRepair && ( + + The app registration ({data.appId}) was created successfully but the + client secret could not be generated. Click Repair to + retry the secret on the existing app, or Recreate to + start over with a fresh app registration. + + )} + {isOrphanedError && ( + + A previous attempt to set up SSO did not save an App ID, so there's + nothing to repair. An orphaned CIPP-SSO app + registration may exist in your Entra tenant — you can delete it + manually. Click Create SSO App to provision a fresh + app registration. + + )} + + )} @@ -158,6 +229,7 @@ export const CippSSOSettings = () => { name="multiTenant" label="Multi-tenant mode (allow users from multiple Entra ID tenants)" formControl={formControl} + disabled={!isProvisioned && !showCreate} /> @@ -167,7 +239,7 @@ export const CippSSOSettings = () => { {!ssoStatus.isLoading && ( - {showCreate ? ( + {showCreate && ( - ) : ( + )} + + {canRepair && ( + <> + + + + )} + + {isProvisioned && ( <>