@stigmer/react 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +22 -32
- package/composer/SessionComposer.js.map +1 -1
- package/github/index.d.ts +1 -1
- package/github/index.d.ts.map +1 -1
- package/github/index.js.map +1 -1
- package/github/useGitHubConnection.d.ts +70 -1
- package/github/useGitHubConnection.d.ts.map +1 -1
- package/github/useGitHubConnection.js +99 -20
- package/github/useGitHubConnection.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +19 -3
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +23 -2
- package/organization/OrgProfilePanel.js.map +1 -1
- package/package.json +4 -4
- package/runner/RunnerFileBrowser.d.ts +11 -1
- package/runner/RunnerFileBrowser.d.ts.map +1 -1
- package/runner/RunnerFileBrowser.js +70 -7
- package/runner/RunnerFileBrowser.js.map +1 -1
- package/runner/WorkspaceRunnerSelector.d.ts +36 -0
- package/runner/WorkspaceRunnerSelector.d.ts.map +1 -0
- package/runner/WorkspaceRunnerSelector.js +63 -0
- package/runner/WorkspaceRunnerSelector.js.map +1 -0
- package/runner/index.d.ts +2 -0
- package/runner/index.d.ts.map +1 -1
- package/runner/index.js +1 -0
- package/runner/index.js.map +1 -1
- package/runner/useRunnerFileBrowser.d.ts.map +1 -1
- package/runner/useRunnerFileBrowser.js +26 -2
- package/runner/useRunnerFileBrowser.js.map +1 -1
- package/settings/MembersSection.d.ts.map +1 -1
- package/settings/MembersSection.js +7 -2
- package/settings/MembersSection.js.map +1 -1
- package/src/composer/SessionComposer.tsx +46 -43
- package/src/github/index.ts +1 -0
- package/src/github/useGitHubConnection.ts +162 -22
- package/src/identity-provider/IdentityProviderWizard.tsx +112 -3
- package/src/index.ts +1 -0
- package/src/organization/OrgProfilePanel.tsx +98 -0
- package/src/runner/RunnerFileBrowser.tsx +227 -8
- package/src/runner/WorkspaceRunnerSelector.tsx +180 -0
- package/src/runner/index.ts +3 -0
- package/src/runner/useRunnerFileBrowser.ts +39 -3
- package/src/settings/MembersSection.tsx +23 -1
- package/src/workspace/WorkspaceEditor.tsx +176 -126
- package/src/workspace/index.ts +5 -0
- package/src/workspace/useRecentWorkspaces.ts +162 -0
- package/src/workspace/useWorkspaceEntries.ts +13 -0
- package/styles.css +1 -1
- package/workspace/WorkspaceEditor.d.ts +25 -22
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +64 -43
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/index.d.ts +2 -0
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -0
- package/workspace/index.js.map +1 -1
- package/workspace/useRecentWorkspaces.d.ts +31 -0
- package/workspace/useRecentWorkspaces.d.ts.map +1 -0
- package/workspace/useRecentWorkspaces.js +117 -0
- package/workspace/useRecentWorkspaces.js.map +1 -0
- package/workspace/useWorkspaceEntries.d.ts +8 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js +4 -0
- package/workspace/useWorkspaceEntries.js.map +1 -1
|
@@ -42,6 +42,52 @@ export interface GitHubConnectOptions {
|
|
|
42
42
|
readonly popup?: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Optional configuration for {@link useGitHubConnection}.
|
|
47
|
+
*
|
|
48
|
+
* Enables desktop and non-browser environments to participate in the
|
|
49
|
+
* GitHub OAuth flow without relying on `window.open()` popups.
|
|
50
|
+
*/
|
|
51
|
+
export interface UseGitHubConnectionConfig {
|
|
52
|
+
/**
|
|
53
|
+
* Custom function to open the authorization URL in a browser.
|
|
54
|
+
*
|
|
55
|
+
* When provided, the hook calls this instead of `window.open()` during
|
|
56
|
+
* popup-mode `connect()` flows. This enables desktop environments
|
|
57
|
+
* (e.g. Tauri, Electron) where webview popups are blocked but the
|
|
58
|
+
* system browser is available.
|
|
59
|
+
*
|
|
60
|
+
* When used with {@link callbackUrl}, the callback page processes the
|
|
61
|
+
* token exchange and the consumer calls {@link UseGitHubConnectionReturn.reconcile}
|
|
62
|
+
* to pick up the token from the personal environment.
|
|
63
|
+
*
|
|
64
|
+
* When used without {@link callbackUrl} (e.g. localhost callback server),
|
|
65
|
+
* the consumer calls {@link UseGitHubConnectionReturn.handleCallback}
|
|
66
|
+
* with the code, state, and redirect URI to complete the exchange.
|
|
67
|
+
*/
|
|
68
|
+
readonly openUrl?: (url: string) => void | Promise<void>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Override the OAuth callback URL.
|
|
72
|
+
*
|
|
73
|
+
* When provided, this replaces the `redirectUri` parameter passed to
|
|
74
|
+
* `connect()` by UI components like `WorkspaceEditor`. Use this to
|
|
75
|
+
* route the GitHub callback to a specific URL — for example, the
|
|
76
|
+
* Stigmer web console's callback page for desktop flows, or a
|
|
77
|
+
* localhost server for local development.
|
|
78
|
+
*
|
|
79
|
+
* When the callback page handles the token exchange itself (cloud
|
|
80
|
+
* desktop flows), the consumer triggers re-reconciliation via
|
|
81
|
+
* {@link UseGitHubConnectionReturn.reconcile} after the callback
|
|
82
|
+
* completes externally.
|
|
83
|
+
*
|
|
84
|
+
* When the consumer handles the exchange (localhost flows), the same
|
|
85
|
+
* URL must be passed to `handleCallback()` — GitHub requires an
|
|
86
|
+
* exact match between the authorize and exchange redirect URIs.
|
|
87
|
+
*/
|
|
88
|
+
readonly callbackUrl?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
45
91
|
/** Return value of {@link useGitHubConnection}. */
|
|
46
92
|
export interface UseGitHubConnectionReturn {
|
|
47
93
|
/** Whether a valid GitHub token exists. */
|
|
@@ -72,6 +118,16 @@ export interface UseGitHubConnectionReturn {
|
|
|
72
118
|
state: string,
|
|
73
119
|
redirectUri: string,
|
|
74
120
|
) => Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Trigger re-reconciliation from the personal environment.
|
|
123
|
+
*
|
|
124
|
+
* Call this when the token exchange was handled externally (e.g. by
|
|
125
|
+
* the Stigmer web callback page during a desktop OAuth flow) and the
|
|
126
|
+
* token is already stored server-side. The hook will refetch the
|
|
127
|
+
* personal environment, reveal the token, and update
|
|
128
|
+
* {@link isConnected}.
|
|
129
|
+
*/
|
|
130
|
+
readonly reconcile: () => void;
|
|
75
131
|
/** Clear the stored token and user info. */
|
|
76
132
|
readonly disconnect: () => void;
|
|
77
133
|
}
|
|
@@ -93,6 +149,39 @@ async function fetchGitHubUser(token: string): Promise<GitHubUser | null> {
|
|
|
93
149
|
}
|
|
94
150
|
}
|
|
95
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Reads the OAuth state from the opener window's sessionStorage when
|
|
154
|
+
* running inside a popup (same-origin). Falls back to the current
|
|
155
|
+
* window's sessionStorage for redirect-based flows or when the
|
|
156
|
+
* opener is unavailable.
|
|
157
|
+
*/
|
|
158
|
+
function getSavedOAuthState(): string | null {
|
|
159
|
+
try {
|
|
160
|
+
if (window.opener && !window.opener.closed) {
|
|
161
|
+
const openerState = window.opener.sessionStorage.getItem(
|
|
162
|
+
STORAGE_KEY_STATE,
|
|
163
|
+
);
|
|
164
|
+
if (openerState) return openerState;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Cross-origin or closed opener — fall through to local storage.
|
|
168
|
+
}
|
|
169
|
+
return sessionStorage.getItem(STORAGE_KEY_STATE);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Removes the OAuth state key from both the current window's and the
|
|
174
|
+
* opener's sessionStorage (best-effort).
|
|
175
|
+
*/
|
|
176
|
+
function clearOAuthState(): void {
|
|
177
|
+
sessionStorage.removeItem(STORAGE_KEY_STATE);
|
|
178
|
+
try {
|
|
179
|
+
window.opener?.sessionStorage?.removeItem(STORAGE_KEY_STATE);
|
|
180
|
+
} catch {
|
|
181
|
+
// Cross-origin or closed opener — ignore.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
96
185
|
/**
|
|
97
186
|
* Checks whether the personal environment's redacted data contains a given key.
|
|
98
187
|
* The key is present even when the value is redacted (`***REDACTED***`).
|
|
@@ -127,6 +216,8 @@ function personalEnvHasKey(
|
|
|
127
216
|
*
|
|
128
217
|
* @param org - The active organization slug. Required for server-side
|
|
129
218
|
* token storage. Pass `null` to skip all server operations.
|
|
219
|
+
* @param config - Optional configuration for desktop / non-browser
|
|
220
|
+
* environments. See {@link UseGitHubConnectionConfig}.
|
|
130
221
|
*
|
|
131
222
|
* @example
|
|
132
223
|
* ```tsx
|
|
@@ -155,9 +246,23 @@ function personalEnvHasKey(
|
|
|
155
246
|
* );
|
|
156
247
|
* }
|
|
157
248
|
* ```
|
|
249
|
+
*
|
|
250
|
+
* @example Desktop (Tauri) — open in system browser, callback via deep link
|
|
251
|
+
* ```tsx
|
|
252
|
+
* function DesktopGitHubConnect({ org }: { org: string }) {
|
|
253
|
+
* const gh = useGitHubConnection(org, {
|
|
254
|
+
* openUrl: (url) => invoke("open_auth_in_browser", { authUrl: url }),
|
|
255
|
+
* callbackUrl: "https://app.stigmer.ai/auth/github/callback?source=desktop",
|
|
256
|
+
* });
|
|
257
|
+
* // The web callback page processes the exchange, then redirects to
|
|
258
|
+
* // stigmer://github/callback-done. The desktop deep link handler
|
|
259
|
+
* // calls gh.reconcile() to pick up the token.
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
158
262
|
*/
|
|
159
263
|
export function useGitHubConnection(
|
|
160
264
|
org: string | null,
|
|
265
|
+
config?: UseGitHubConnectionConfig,
|
|
161
266
|
): UseGitHubConnectionReturn {
|
|
162
267
|
const stigmer = useStigmer();
|
|
163
268
|
const [token, setToken] = useState<string | null>(null);
|
|
@@ -287,8 +392,12 @@ export function useGitHubConnection(
|
|
|
287
392
|
|
|
288
393
|
const connect = useCallback(
|
|
289
394
|
async (redirectUri: string, options?: GitHubConnectOptions) => {
|
|
395
|
+
const effectiveRedirectUri = config?.callbackUrl ?? redirectUri;
|
|
396
|
+
|
|
290
397
|
const { authorizeUrl, state } =
|
|
291
|
-
await stigmer.github.getOAuthAuthorizeUrl({
|
|
398
|
+
await stigmer.github.getOAuthAuthorizeUrl({
|
|
399
|
+
redirectUri: effectiveRedirectUri,
|
|
400
|
+
});
|
|
292
401
|
|
|
293
402
|
sessionStorage.setItem(STORAGE_KEY_STATE, state);
|
|
294
403
|
|
|
@@ -297,7 +406,18 @@ export function useGitHubConnection(
|
|
|
297
406
|
return;
|
|
298
407
|
}
|
|
299
408
|
|
|
300
|
-
//
|
|
409
|
+
// ── External opener (desktop / non-browser) ─────────────────────
|
|
410
|
+
// When `config.openUrl` is provided, delegate to the consumer
|
|
411
|
+
// instead of using `window.open()`. The consumer is responsible
|
|
412
|
+
// for delivering the callback data via `handleCallback()` or
|
|
413
|
+
// triggering re-reconciliation via `reconcile()`.
|
|
414
|
+
if (config?.openUrl) {
|
|
415
|
+
setIsConnecting(true);
|
|
416
|
+
await config.openUrl(authorizeUrl);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Browser popup ───────────────────────────────────────────────
|
|
301
421
|
if (popupRef.current && !popupRef.current.closed) {
|
|
302
422
|
popupRef.current.focus();
|
|
303
423
|
return;
|
|
@@ -327,43 +447,62 @@ export function useGitHubConnection(
|
|
|
327
447
|
popupPollRef.current = null;
|
|
328
448
|
popupRef.current = null;
|
|
329
449
|
setIsConnecting(false);
|
|
450
|
+
|
|
451
|
+
// The callback page may have exchanged the code and stored
|
|
452
|
+
// the token server-side (e.g. cross-origin popup flow for
|
|
453
|
+
// platform builders). Re-reconcile from the personal
|
|
454
|
+
// environment to pick up the token.
|
|
455
|
+
setIsLoading(true);
|
|
456
|
+
reconciled.current = false;
|
|
457
|
+
personalEnvRef.current.refetch();
|
|
330
458
|
}
|
|
331
459
|
}, POPUP_CLOSE_POLL_MS);
|
|
332
460
|
popupPollRef.current = pollId;
|
|
333
461
|
},
|
|
334
|
-
[stigmer],
|
|
462
|
+
[stigmer, config?.openUrl, config?.callbackUrl],
|
|
335
463
|
);
|
|
336
464
|
|
|
337
465
|
const handleCallback = useCallback(
|
|
338
466
|
async (code: string, state: string, redirectUri: string) => {
|
|
339
|
-
const savedState =
|
|
467
|
+
const savedState = getSavedOAuthState();
|
|
340
468
|
if (savedState && savedState !== state) {
|
|
341
469
|
throw new Error("OAuth state mismatch — possible CSRF attack");
|
|
342
470
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const { accessToken } = await stigmer.github.exchangeOAuthCode({
|
|
346
|
-
code,
|
|
347
|
-
state,
|
|
348
|
-
redirectUri,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const tokenVar = {
|
|
352
|
-
[GITHUB_TOKEN_KEY]: { value: accessToken, isSecret: true },
|
|
353
|
-
};
|
|
354
|
-
const env = await personalEnvRef.current.getOrCreate(tokenVar);
|
|
355
|
-
if (!personalEnvHasKey(env, GITHUB_TOKEN_KEY)) {
|
|
356
|
-
await personalEnvRef.current.addVariables(tokenVar);
|
|
357
|
-
}
|
|
471
|
+
clearOAuthState();
|
|
358
472
|
|
|
359
|
-
|
|
473
|
+
try {
|
|
474
|
+
const { accessToken } = await stigmer.github.exchangeOAuthCode({
|
|
475
|
+
code,
|
|
476
|
+
state,
|
|
477
|
+
redirectUri,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const tokenVar = {
|
|
481
|
+
[GITHUB_TOKEN_KEY]: { value: accessToken, isSecret: true },
|
|
482
|
+
};
|
|
483
|
+
const env = await personalEnvRef.current.getOrCreate(tokenVar);
|
|
484
|
+
if (!personalEnvHasKey(env, GITHUB_TOKEN_KEY)) {
|
|
485
|
+
await personalEnvRef.current.addVariables(tokenVar);
|
|
486
|
+
}
|
|
360
487
|
|
|
361
|
-
|
|
362
|
-
|
|
488
|
+
setToken(accessToken);
|
|
489
|
+
|
|
490
|
+
const u = await fetchGitHubUser(accessToken);
|
|
491
|
+
setUser(u);
|
|
492
|
+
} finally {
|
|
493
|
+
setIsConnecting(false);
|
|
494
|
+
}
|
|
363
495
|
},
|
|
364
496
|
[stigmer],
|
|
365
497
|
);
|
|
366
498
|
|
|
499
|
+
const reconcile = useCallback(() => {
|
|
500
|
+
setIsConnecting(false);
|
|
501
|
+
setIsLoading(true);
|
|
502
|
+
reconciled.current = false;
|
|
503
|
+
personalEnvRef.current.refetch();
|
|
504
|
+
}, []);
|
|
505
|
+
|
|
367
506
|
const disconnect = useCallback(() => {
|
|
368
507
|
sessionStorage.removeItem(STORAGE_KEY_STATE);
|
|
369
508
|
setToken(null);
|
|
@@ -396,6 +535,7 @@ export function useGitHubConnection(
|
|
|
396
535
|
token,
|
|
397
536
|
connect,
|
|
398
537
|
handleCallback,
|
|
538
|
+
reconcile,
|
|
399
539
|
disconnect,
|
|
400
540
|
};
|
|
401
541
|
}
|
|
@@ -26,7 +26,7 @@ export interface IdentityProviderWizardProps {
|
|
|
26
26
|
readonly className?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
type WizardStep = "pick" | "configure" | "review";
|
|
29
|
+
type WizardStep = "pick" | "configure" | "review" | "success";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Multi-step wizard for creating a new identity provider.
|
|
@@ -88,6 +88,9 @@ export function IdentityProviderWizard({
|
|
|
88
88
|
const [autoGrantRole, setAutoGrantRole] = useState<IamRole>(IamRole.iam_role_unspecified);
|
|
89
89
|
const [tenantOrgClaim, setTenantOrgClaim] = useState("");
|
|
90
90
|
|
|
91
|
+
// Success step
|
|
92
|
+
const [createdIdp, setCreatedIdp] = useState<IdentityProvider | null>(null);
|
|
93
|
+
|
|
91
94
|
// -- Step transitions ------------------------------------------------
|
|
92
95
|
|
|
93
96
|
const handlePickProvider = useCallback((selected: ProviderPreset) => {
|
|
@@ -186,7 +189,8 @@ export function IdentityProviderWizard({
|
|
|
186
189
|
}),
|
|
187
190
|
}),
|
|
188
191
|
});
|
|
189
|
-
|
|
192
|
+
setCreatedIdp(idp);
|
|
193
|
+
setStep("success");
|
|
190
194
|
} catch {
|
|
191
195
|
// error state is managed by useCreateIdentityProvider
|
|
192
196
|
}
|
|
@@ -194,7 +198,7 @@ export function IdentityProviderWizard({
|
|
|
194
198
|
[
|
|
195
199
|
name, org, jwksUri, issuers, audience, userinfoEndpoint,
|
|
196
200
|
isSso, oidcClientId, autoProvision, autoGrant, autoGrantRole,
|
|
197
|
-
tenantOrgClaim, create, clearError,
|
|
201
|
+
tenantOrgClaim, create, clearError,
|
|
198
202
|
],
|
|
199
203
|
);
|
|
200
204
|
|
|
@@ -267,6 +271,18 @@ export function IdentityProviderWizard({
|
|
|
267
271
|
onCancel={onCancel}
|
|
268
272
|
/>
|
|
269
273
|
)}
|
|
274
|
+
|
|
275
|
+
{step === "success" && createdIdp && (
|
|
276
|
+
<SuccessStep
|
|
277
|
+
identityProvider={createdIdp}
|
|
278
|
+
org={org}
|
|
279
|
+
isSso={isSso}
|
|
280
|
+
autoProvision={autoProvision}
|
|
281
|
+
autoGrant={autoGrant}
|
|
282
|
+
autoGrantRole={autoGrantRole}
|
|
283
|
+
onDone={() => onCreated?.(createdIdp)}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
270
286
|
</div>
|
|
271
287
|
);
|
|
272
288
|
}
|
|
@@ -279,6 +295,7 @@ const STEPS: { key: WizardStep; label: string }[] = [
|
|
|
279
295
|
{ key: "pick", label: "Provider" },
|
|
280
296
|
{ key: "configure", label: "Configure" },
|
|
281
297
|
{ key: "review", label: "Review" },
|
|
298
|
+
{ key: "success", label: "Done" },
|
|
282
299
|
];
|
|
283
300
|
|
|
284
301
|
function StepIndicator({ current }: { current: WizardStep }) {
|
|
@@ -608,6 +625,98 @@ function ReviewStep({
|
|
|
608
625
|
);
|
|
609
626
|
}
|
|
610
627
|
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Success step (step 4)
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
function SuccessStep({
|
|
633
|
+
identityProvider,
|
|
634
|
+
org,
|
|
635
|
+
isSso,
|
|
636
|
+
autoProvision,
|
|
637
|
+
autoGrant,
|
|
638
|
+
autoGrantRole,
|
|
639
|
+
onDone,
|
|
640
|
+
}: {
|
|
641
|
+
identityProvider: IdentityProvider;
|
|
642
|
+
org: string;
|
|
643
|
+
isSso: boolean;
|
|
644
|
+
autoProvision: boolean;
|
|
645
|
+
autoGrant: boolean;
|
|
646
|
+
autoGrantRole: IamRole;
|
|
647
|
+
onDone: () => void;
|
|
648
|
+
}) {
|
|
649
|
+
const displayName =
|
|
650
|
+
identityProvider.spec?.displayName ||
|
|
651
|
+
identityProvider.metadata?.name ||
|
|
652
|
+
"Identity provider";
|
|
653
|
+
|
|
654
|
+
const roleName =
|
|
655
|
+
autoGrantRole !== IamRole.iam_role_unspecified
|
|
656
|
+
? IamRole[autoGrantRole]
|
|
657
|
+
: "viewer";
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
<div className="space-y-4">
|
|
661
|
+
<div className="rounded-md border border-primary/30 bg-primary-subtle px-3 py-2.5">
|
|
662
|
+
<p className="text-xs font-medium text-foreground">
|
|
663
|
+
{displayName} created successfully
|
|
664
|
+
</p>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div className="space-y-2">
|
|
668
|
+
<p className="text-xs font-medium text-foreground">What happens next</p>
|
|
669
|
+
|
|
670
|
+
{isSso ? (
|
|
671
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
672
|
+
Users can sign in via SSO at{" "}
|
|
673
|
+
<span className="font-mono text-foreground">
|
|
674
|
+
/login?org={org}
|
|
675
|
+
</span>
|
|
676
|
+
. Accounts are auto-provisioned and granted the{" "}
|
|
677
|
+
<span className="font-medium text-foreground">viewer</span> role on
|
|
678
|
+
this organization.
|
|
679
|
+
</p>
|
|
680
|
+
) : autoProvision && autoGrant ? (
|
|
681
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
682
|
+
Users authenticating with JWTs from this provider will be
|
|
683
|
+
automatically provisioned and granted the{" "}
|
|
684
|
+
<span className="font-medium text-foreground">{roleName}</span> role
|
|
685
|
+
on this organization. No additional setup is required.
|
|
686
|
+
</p>
|
|
687
|
+
) : autoProvision ? (
|
|
688
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
689
|
+
Accounts are auto-provisioned on first authentication, but no
|
|
690
|
+
organization role is granted automatically. Use the Members page to
|
|
691
|
+
grant access.
|
|
692
|
+
</p>
|
|
693
|
+
) : (
|
|
694
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
695
|
+
The trust relationship is configured. Accounts must be created
|
|
696
|
+
manually before users can authenticate.
|
|
697
|
+
</p>
|
|
698
|
+
)}
|
|
699
|
+
|
|
700
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
701
|
+
To verify the setup, have a user authenticate with a JWT from this
|
|
702
|
+
provider and confirm they can access the organization's resources.
|
|
703
|
+
</p>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<button
|
|
707
|
+
type="button"
|
|
708
|
+
onClick={onDone}
|
|
709
|
+
className={cn(
|
|
710
|
+
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
711
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
712
|
+
)}
|
|
713
|
+
>
|
|
714
|
+
Done
|
|
715
|
+
</button>
|
|
716
|
+
</div>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
611
720
|
// ---------------------------------------------------------------------------
|
|
612
721
|
// Shared primitives
|
|
613
722
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { getUserMessage } from "@stigmer/sdk";
|
|
|
12
12
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
13
13
|
import { useOrganization } from "./useOrganization";
|
|
14
14
|
import { useUpdateOrganization } from "./useUpdateOrganization";
|
|
15
|
+
import { useIdentityProviderList } from "../identity-provider/useIdentityProviderList";
|
|
16
|
+
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode";
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
// Constants
|
|
@@ -338,10 +340,106 @@ export function OrgProfilePanel({
|
|
|
338
340
|
</button>
|
|
339
341
|
)}
|
|
340
342
|
</div>
|
|
343
|
+
|
|
344
|
+
{/* -- Identity Providers summary -- */}
|
|
345
|
+
<IdentityProvidersSummary orgSlug={serverSlug} />
|
|
341
346
|
</form>
|
|
342
347
|
);
|
|
343
348
|
}
|
|
344
349
|
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// IdentityProvidersSummary — shows linked IDPs on the org profile
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
function IdentityProvidersSummary({ orgSlug }: { orgSlug: string }) {
|
|
355
|
+
const idpAvailable = useResourceAvailable(ApiResourceKind.identity_provider);
|
|
356
|
+
const { identityProviders, isLoading } = useIdentityProviderList(
|
|
357
|
+
idpAvailable && orgSlug ? orgSlug : null,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (!idpAvailable || !orgSlug) return null;
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<>
|
|
364
|
+
<hr className="border-border" />
|
|
365
|
+
<div className="space-y-2">
|
|
366
|
+
<p className="text-[0.65rem] font-medium text-muted-foreground uppercase tracking-wider">
|
|
367
|
+
Identity Providers
|
|
368
|
+
</p>
|
|
369
|
+
|
|
370
|
+
{isLoading ? (
|
|
371
|
+
<div className="bg-muted-subtle h-8 animate-pulse rounded" />
|
|
372
|
+
) : identityProviders.length === 0 ? (
|
|
373
|
+
<p className="text-xs text-muted-foreground">
|
|
374
|
+
No identity providers configured.{" "}
|
|
375
|
+
<a
|
|
376
|
+
href="/settings/identity-providers"
|
|
377
|
+
className="text-primary hover:text-primary-hover underline underline-offset-2"
|
|
378
|
+
>
|
|
379
|
+
Set up federated authentication
|
|
380
|
+
</a>
|
|
381
|
+
</p>
|
|
382
|
+
) : (
|
|
383
|
+
<div className="space-y-1.5">
|
|
384
|
+
{identityProviders.map((idp) => {
|
|
385
|
+
const spec = idp.spec;
|
|
386
|
+
const displayName =
|
|
387
|
+
spec?.displayName || idp.metadata?.name || "Unnamed";
|
|
388
|
+
const isSso = spec?.isSsoProvider;
|
|
389
|
+
const isJit = !isSso && spec?.autoProvisionAccounts;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
key={idp.metadata?.id}
|
|
394
|
+
className="flex items-center gap-2 text-xs"
|
|
395
|
+
>
|
|
396
|
+
<ShieldSmallIcon />
|
|
397
|
+
<span className="text-foreground truncate">{displayName}</span>
|
|
398
|
+
{isSso && (
|
|
399
|
+
<span className="rounded-full border border-primary/30 bg-primary-subtle px-1.5 py-px text-[0.6rem] font-medium text-primary">
|
|
400
|
+
SSO
|
|
401
|
+
</span>
|
|
402
|
+
)}
|
|
403
|
+
{isJit && (
|
|
404
|
+
<span className="rounded-full border border-primary/30 bg-primary-subtle px-1.5 py-px text-[0.6rem] font-medium text-primary">
|
|
405
|
+
JIT
|
|
406
|
+
</span>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
})}
|
|
411
|
+
<a
|
|
412
|
+
href="/settings/identity-providers"
|
|
413
|
+
className="inline-block text-[0.65rem] text-primary hover:text-primary-hover underline underline-offset-2"
|
|
414
|
+
>
|
|
415
|
+
Manage
|
|
416
|
+
</a>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
</>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function ShieldSmallIcon() {
|
|
425
|
+
return (
|
|
426
|
+
<svg
|
|
427
|
+
width="12"
|
|
428
|
+
height="12"
|
|
429
|
+
viewBox="0 0 16 16"
|
|
430
|
+
fill="none"
|
|
431
|
+
stroke="currentColor"
|
|
432
|
+
strokeWidth="1.5"
|
|
433
|
+
strokeLinecap="round"
|
|
434
|
+
strokeLinejoin="round"
|
|
435
|
+
aria-hidden="true"
|
|
436
|
+
className="shrink-0 text-muted-foreground"
|
|
437
|
+
>
|
|
438
|
+
<path d="M8 1.5L2 4v4c0 3.5 2.5 5.5 6 7 3.5-1.5 6-3.5 6-7V4L8 1.5z" />
|
|
439
|
+
</svg>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
345
443
|
// ---------------------------------------------------------------------------
|
|
346
444
|
// ReadOnlyField — copiable label/value pair
|
|
347
445
|
// ---------------------------------------------------------------------------
|