1- import { useEffect , useState } from "react" ;
1+ import { useEffect } from "react" ;
22import {
33 Alert ,
44 Button ,
55 CardActions ,
66 CardContent ,
7- CardHeader ,
87 Chip ,
98 Divider ,
109 Skeleton ,
@@ -20,16 +19,16 @@ import { CippApiResults } from "../CippComponents/CippApiResults";
2019
2120const statusLabels = {
2221 none : { label : "Not Configured" , color : "default" } ,
23- app_created : { label : "App Created" , color : "info " } ,
24- appid_stored : { label : "App ID Stored" , color : "info " } ,
22+ app_created : { label : "App Created — Secret Pending " , color : "warning " } ,
23+ appid_stored : { label : "App ID Stored — Secret Pending " , color : "warning " } ,
2524 secrets_stored : { label : "Secrets Stored" , color : "success" } ,
2625 complete : { label : "Complete" , color : "success" } ,
2726 error : { label : "Error" , color : "error" } ,
2827} ;
2928
30- export const CippSSOSettings = ( ) => {
31- const [ showCreate , setShowCreate ] = useState ( false ) ;
29+ const repairableStatuses = new Set ( [ "error" , "app_created" , "appid_stored" ] ) ;
3230
31+ export const CippSSOSettings = ( ) => {
3332 const formControl = useForm ( {
3433 mode : "onChange" ,
3534 defaultValues : { multiTenant : false } ,
@@ -49,32 +48,82 @@ export const CippSSOSettings = () => {
4948 if ( ssoStatus . isSuccess && ssoStatus . data ?. Results ) {
5049 const data = ssoStatus . data . Results ;
5150 formControl . reset ( { multiTenant : data . multiTenant ?? false } ) ;
52- setShowCreate ( ! data . configured ) ;
5351 }
5452 } , [ ssoStatus . isSuccess , ssoStatus . data ] ) ;
5553
56- const handleUpdate = ( ) => {
54+ const data = ssoStatus . data ?. Results ;
55+ const statusKey = data ?. status ?? "none" ;
56+ const statusInfo = statusLabels [ statusKey ] ?? statusLabels . none ;
57+ const hasAppId = Boolean ( data ?. appId ) ;
58+ // Server-provided canRepair is authoritative when present; fall back to local heuristic.
59+ const canRepair =
60+ data ?. canRepair ??
61+ ( hasAppId && repairableStatuses . has ( statusKey ) ) ;
62+ const isProvisioned =
63+ statusKey === "complete" || ( statusKey === "secrets_stored" && hasAppId ) ;
64+ // Show "Create SSO App" whenever there isn't a working app AND there's nothing to repair —
65+ // covers fresh installs AND legacy broken installs where the AppId was never persisted
66+ // (the original "Failed to create client secret after 5 attempts" bug).
67+ const showCreate = ! isProvisioned && ! canRepair ;
68+ const isOrphanedError = statusKey === "error" && ! hasAppId ;
69+
70+ const handleCreate = ( ) => {
71+ ssoAction . mutate ( {
72+ url : "/api/ExecSSOSetup" ,
73+ data : {
74+ Action : "Create" ,
75+ multiTenant : formControl . getValues ( "multiTenant" ) ,
76+ } ,
77+ } ) ;
78+ } ;
79+
80+ const handleRepair = ( ) => {
81+ ssoAction . mutate ( {
82+ url : "/api/ExecSSOSetup" ,
83+ data : { Action : "Repair" } ,
84+ } ) ;
85+ } ;
86+
87+ const handleRecreate = ( ) => {
5788 if (
5889 ! window . confirm (
59- "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue ?"
90+ "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 ?"
6091 )
6192 ) {
6293 return ;
6394 }
64- ssoAction . mutate ( {
65- url : "/api/ExecSSOSetup" ,
66- data : {
67- Action : "Update " ,
68- multiTenant : formControl . getValues ( "multiTenant" ) ,
95+ // Clear first, then create. ApiPostCall chains via the success refetch — call sequentially.
96+ ssoAction . mutate (
97+ {
98+ url : "/api/ExecSSOSetup " ,
99+ data : { Action : "Recreate" } ,
69100 } ,
70- } ) ;
101+ {
102+ onSuccess : ( ) => {
103+ ssoAction . mutate ( {
104+ url : "/api/ExecSSOSetup" ,
105+ data : {
106+ Action : "Create" ,
107+ multiTenant : formControl . getValues ( "multiTenant" ) ,
108+ } ,
109+ } ) ;
110+ } ,
111+ }
112+ ) ;
71113 } ;
72114
73- const handleCreate = ( ) => {
115+ const handleUpdate = ( ) => {
116+ if (
117+ ! window . confirm (
118+ "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?"
119+ )
120+ ) {
121+ return ;
122+ }
74123 ssoAction . mutate ( {
75124 url : "/api/ExecSSOSetup" ,
76125 data : {
77- Action : "Create " ,
126+ Action : "Update " ,
78127 multiTenant : formControl . getValues ( "multiTenant" ) ,
79128 } ,
80129 } ) ;
@@ -87,9 +136,6 @@ export const CippSSOSettings = () => {
87136 } ) ;
88137 } ;
89138
90- const data = ssoStatus . data ?. Results ;
91- const statusInfo = statusLabels [ data ?. status ] ?? statusLabels . none ;
92-
93139 return (
94140 < CippButtonCard title = "SSO App Registration" isFetching = { ssoStatus . isFetching } >
95141 < CardContent >
@@ -141,13 +187,38 @@ export const CippSSOSettings = () => {
141187 ) }
142188
143189 { data ?. lastError && (
144- < >
145- < Grid size = { { xs : 12 } } >
146- < Alert severity = "error" sx = { { mt : 1 } } >
147- { data . lastError }
148- </ Alert >
149- </ Grid >
150- </ >
190+ < Grid size = { { xs : 12 } } >
191+ < Alert
192+ severity = { canRepair ? "warning" : "error" }
193+ sx = { { mt : 1 } }
194+ >
195+ < Typography variant = "body2" sx = { { fontWeight : 600 , mb : 0.5 } } >
196+ { canRepair
197+ ? "Setup did not finish"
198+ : isOrphanedError
199+ ? "Previous setup failed"
200+ : "Error" }
201+ </ Typography >
202+ < Typography variant = "body2" > { data . lastError } </ Typography >
203+ { canRepair && (
204+ < Typography variant = "caption" sx = { { display : "block" , mt : 1 } } >
205+ The app registration ({ data . appId } ) was created successfully but the
206+ client secret could not be generated. Click < strong > Repair</ strong > to
207+ retry the secret on the existing app, or < strong > Recreate</ strong > to
208+ start over with a fresh app registration.
209+ </ Typography >
210+ ) }
211+ { isOrphanedError && (
212+ < Typography variant = "caption" sx = { { display : "block" , mt : 1 } } >
213+ A previous attempt to set up SSO did not save an App ID, so there's
214+ nothing to repair. An orphaned < strong > CIPP-SSO</ strong > app
215+ registration may exist in your Entra tenant — you can delete it
216+ manually. Click < strong > Create SSO App</ strong > to provision a fresh
217+ app registration.
218+ </ Typography >
219+ ) }
220+ </ Alert >
221+ </ Grid >
151222 ) }
152223 </ Grid >
153224
@@ -158,6 +229,7 @@ export const CippSSOSettings = () => {
158229 name = "multiTenant"
159230 label = "Multi-tenant mode (allow users from multiple Entra ID tenants)"
160231 formControl = { formControl }
232+ disabled = { ! isProvisioned && ! showCreate }
161233 />
162234
163235 < CippApiResults apiObject = { ssoAction } />
@@ -167,15 +239,38 @@ export const CippSSOSettings = () => {
167239 { ! ssoStatus . isLoading && (
168240 < CardActions sx = { { justifyContent : "flex-end" , px : 2 , pb : 2 } } >
169241 < Stack direction = "row" spacing = { 1 } >
170- { showCreate ? (
242+ { showCreate && (
171243 < Button
172244 variant = "contained"
173245 onClick = { handleCreate }
174246 disabled = { ssoAction . isPending }
175247 >
176248 Create SSO App
177249 </ Button >
178- ) : (
250+ ) }
251+
252+ { canRepair && (
253+ < >
254+ < Button
255+ variant = "outlined"
256+ color = "warning"
257+ onClick = { handleRecreate }
258+ disabled = { ssoAction . isPending }
259+ >
260+ Recreate
261+ </ Button >
262+ < Button
263+ variant = "contained"
264+ color = "warning"
265+ onClick = { handleRepair }
266+ disabled = { ssoAction . isPending }
267+ >
268+ Repair
269+ </ Button >
270+ </ >
271+ ) }
272+
273+ { isProvisioned && (
179274 < >
180275 < Button
181276 variant = "outlined"
0 commit comments