@iqauth/sdk 2.7.0 → 2.8.1
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/dist/browser-session.d.mts +3 -3
- package/dist/browser-session.d.ts +3 -3
- package/dist/browser-session.js +31 -5
- package/dist/browser-session.mjs +1 -1
- package/dist/browser.d.mts +3 -3
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +23 -3
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
- package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
- package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
- package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
- package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
- package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
- package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
- package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +5 -5
- package/dist/express.d.ts +5 -5
- package/dist/express.js +217 -36
- package/dist/express.mjs +38 -26
- package/dist/fastify.d.mts +10 -2
- package/dist/fastify.d.ts +10 -2
- package/dist/fastify.js +260 -16
- package/dist/fastify.mjs +80 -5
- package/dist/hono.d.mts +10 -2
- package/dist/hono.d.ts +10 -2
- package/dist/hono.js +240 -16
- package/dist/hono.mjs +60 -5
- package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
- package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +149 -26
- package/dist/index.mjs +5 -5
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +3 -3
- package/dist/mobile.d.ts +3 -3
- package/dist/mobile.js +31 -5
- package/dist/mobile.mjs +1 -1
- package/dist/next.d.mts +10 -2
- package/dist/next.d.ts +10 -2
- package/dist/next.js +212 -11
- package/dist/next.mjs +62 -4
- package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/react-permissions.d.mts +4 -4
- package/dist/react-permissions.d.ts +4 -4
- package/dist/react-permissions.mjs +4 -3
- package/dist/react.d.mts +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js +570 -41
- package/dist/react.mjs +19 -5
- package/dist/server/handlers.d.mts +56 -5
- package/dist/server/handlers.d.ts +56 -5
- package/dist/server/handlers.js +123 -8
- package/dist/server/handlers.mjs +3 -1
- package/dist/server.d.mts +28 -8
- package/dist/server.d.ts +28 -8
- package/dist/server.js +176 -14
- package/dist/server.mjs +9 -4
- package/dist/service.d.mts +3 -3
- package/dist/service.d.ts +3 -3
- package/dist/service.js +31 -5
- package/dist/service.mjs +1 -1
- package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
- package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
- package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
- package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
- package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
- package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
- package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
- package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
- package/dist/webhooks.d.mts +22 -9
- package/dist/webhooks.d.ts +22 -9
- package/dist/webhooks.js +27 -12
- package/dist/webhooks.mjs +1 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/docs/guides/invitations.md +65 -0
- package/package.json +7 -2
package/dist/react.js
CHANGED
|
@@ -821,6 +821,7 @@ __export(react_exports, {
|
|
|
821
821
|
Protect: () => Protect,
|
|
822
822
|
RedirectToSignIn: () => RedirectToSignIn,
|
|
823
823
|
RedirectToSignedIn: () => RedirectToSignedIn,
|
|
824
|
+
ScopeSwitcher: () => ScopeSwitcher,
|
|
824
825
|
SignIn: () => SignIn,
|
|
825
826
|
SignUp: () => SignUp,
|
|
826
827
|
SignedIn: () => SignedIn,
|
|
@@ -830,9 +831,13 @@ __export(react_exports, {
|
|
|
830
831
|
Waitlist: () => Waitlist,
|
|
831
832
|
__useIQAuthInternal: () => __useIQAuthInternal,
|
|
832
833
|
__version__: () => __version__,
|
|
834
|
+
claimSatisfiesScope: () => claimSatisfiesScope,
|
|
833
835
|
isReturnToAllowed: () => isReturnToAllowed,
|
|
834
836
|
isSilentSsoEligible: () => isSilentSsoEligible,
|
|
837
|
+
performScopeSwitch: () => performScopeSwitch,
|
|
838
|
+
performTenantSwitch: () => performTenantSwitch,
|
|
835
839
|
preflightReturnTo: () => preflightReturnTo,
|
|
840
|
+
resolveAfterSignInDestination: () => resolveAfterSignInDestination,
|
|
836
841
|
revokeSession: () => revokeSession,
|
|
837
842
|
sanitizeBrandCss: () => sanitizeBrandCss,
|
|
838
843
|
sanitizeReturnTo: () => sanitizeReturnTo,
|
|
@@ -846,6 +851,7 @@ __export(react_exports, {
|
|
|
846
851
|
useLinkedIdentities: () => useLinkedIdentities,
|
|
847
852
|
useLocale: () => useLocale,
|
|
848
853
|
useMagicLink: () => useMagicLink,
|
|
854
|
+
useMemberships: () => useMemberships,
|
|
849
855
|
useOrganization: () => useOrganization,
|
|
850
856
|
usePasskey: () => usePasskey,
|
|
851
857
|
useResolvedSdkBranding: () => useResolvedSdkBranding,
|
|
@@ -980,7 +986,10 @@ function claimsToSessionUser(claims) {
|
|
|
980
986
|
...claims.email_verified !== void 0 ? { emailVerified: claims.email_verified } : {},
|
|
981
987
|
...claims.given_name !== void 0 ? { givenName: claims.given_name } : {},
|
|
982
988
|
...claims.family_name !== void 0 ? { familyName: claims.family_name } : {},
|
|
983
|
-
...claims.locale !== void 0 ? { locale: claims.locale } : {}
|
|
989
|
+
...claims.locale !== void 0 ? { locale: claims.locale } : {},
|
|
990
|
+
// Task #171 — surface the active source/client scope when the token was
|
|
991
|
+
// minted scoped, so consumers reading useUser().user can branch on it.
|
|
992
|
+
...claims.scopeContext !== void 0 ? { scopeContext: claims.scopeContext } : {}
|
|
984
993
|
};
|
|
985
994
|
}
|
|
986
995
|
var EMPTY = {
|
|
@@ -1355,10 +1364,27 @@ var SessionManager = class {
|
|
|
1355
1364
|
* session and notify subscribers and other tabs.
|
|
1356
1365
|
*/
|
|
1357
1366
|
applyAccessToken(accessToken, refreshToken) {
|
|
1367
|
+
this.adoptAccessToken(accessToken, { refreshToken });
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Task #197 — Adopt an access token that the server has already minted
|
|
1371
|
+
* for us (e.g. from `POST /api/v1/auth/switch-scope`) without contacting
|
|
1372
|
+
* the issuer. Swaps the in-memory token, re-decodes claims, bumps
|
|
1373
|
+
* `version`, schedules proactive refresh, and broadcasts a
|
|
1374
|
+
* `session:update` to peer tabs.
|
|
1375
|
+
*
|
|
1376
|
+
* This is the safe path for any server endpoint that returns a fresh
|
|
1377
|
+
* access token in its JSON body: we want the new claims (scope, roles,
|
|
1378
|
+
* etc.) to take effect immediately, even if the refresh-cookie round-trip
|
|
1379
|
+
* would have failed (network blip, rate limit, signout race). When the
|
|
1380
|
+
* server also rotated the refresh token, pass it via
|
|
1381
|
+
* `opts.refreshToken` so the cookie stays aligned.
|
|
1382
|
+
*/
|
|
1383
|
+
adoptAccessToken(accessToken, opts) {
|
|
1358
1384
|
const claims = decodeClaims(accessToken);
|
|
1359
1385
|
const user = claimsToSessionUser(claims);
|
|
1360
|
-
if (refreshToken) {
|
|
1361
|
-
void Promise.resolve(this.tokenStore.write(refreshToken, { claims }));
|
|
1386
|
+
if (opts?.refreshToken) {
|
|
1387
|
+
void Promise.resolve(this.tokenStore.write(opts.refreshToken, { claims }));
|
|
1362
1388
|
}
|
|
1363
1389
|
this.update({
|
|
1364
1390
|
status: user ? "authenticated" : "unauthenticated",
|
|
@@ -1806,6 +1832,7 @@ var enUS = {
|
|
|
1806
1832
|
"signIn.useDifferentAccount": "Use a different account",
|
|
1807
1833
|
"signIn.selectTenant": "Choose an organization",
|
|
1808
1834
|
"signIn.selectTenantSubtitle": "You belong to multiple organizations. Pick one to continue.",
|
|
1835
|
+
"signIn.selectScope": "Choose scope",
|
|
1809
1836
|
"signIn.dividerOr": "or",
|
|
1810
1837
|
"signIn.preparingExperience": "Preparing your sign-in experience.",
|
|
1811
1838
|
"signIn.applicationUnavailable": "Application unavailable",
|
|
@@ -1894,6 +1921,11 @@ var enUS = {
|
|
|
1894
1921
|
"orgSwitcher.createNew": "Create organization",
|
|
1895
1922
|
"orgSwitcher.manage": "Manage organization",
|
|
1896
1923
|
"orgSwitcher.noOrgs": "You don't belong to any organizations yet.",
|
|
1924
|
+
"orgSwitcher.mfaRequiredTitle": "Two-factor verification required",
|
|
1925
|
+
"orgSwitcher.mfaRequiredBody": "This organization requires an extra verification step. Continue in the hosted sign-in to finish switching.",
|
|
1926
|
+
"orgSwitcher.scopeSelectionRequiredTitle": "Choose what to access",
|
|
1927
|
+
"orgSwitcher.scopeSelectionRequiredBody": "This organization needs you to pick which workspace to open. Continue in the hosted sign-in to choose.",
|
|
1928
|
+
"orgSwitcher.continueInHostedSignIn": "Continue in hosted sign-in \u2192",
|
|
1897
1929
|
"orgProfile.title": "Organization settings",
|
|
1898
1930
|
"orgProfile.generalTab": "General",
|
|
1899
1931
|
"orgProfile.membersTab": "Members",
|
|
@@ -2004,6 +2036,7 @@ function sanitizeReturnTo(input, options = {}) {
|
|
|
2004
2036
|
if (!input || typeof input !== "string") return fallback;
|
|
2005
2037
|
const trimmed = input.trim();
|
|
2006
2038
|
if (!trimmed) return fallback;
|
|
2039
|
+
if (trimmed.includes("\\")) return fallback;
|
|
2007
2040
|
if (trimmed.startsWith("//")) return fallback;
|
|
2008
2041
|
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
2009
2042
|
return trimmed;
|
|
@@ -2489,6 +2522,65 @@ function RedirectToSignIn(props = {}) {
|
|
|
2489
2522
|
}
|
|
2490
2523
|
return null;
|
|
2491
2524
|
}
|
|
2525
|
+
async function performScopeSwitch(manager, base, target) {
|
|
2526
|
+
const res = await manager.fetch(`${base}/api/v1/auth/switch-scope`, {
|
|
2527
|
+
method: "POST",
|
|
2528
|
+
headers: { "Content-Type": "application/json" },
|
|
2529
|
+
body: JSON.stringify({ scopeType: target.type, scopeId: target.id })
|
|
2530
|
+
});
|
|
2531
|
+
const body = await res.json().catch(() => ({}));
|
|
2532
|
+
if (!res.ok) {
|
|
2533
|
+
throw new Error(body?.error?.message || `HTTP ${res.status}`);
|
|
2534
|
+
}
|
|
2535
|
+
const newToken = body?.data?.accessToken;
|
|
2536
|
+
if (newToken) {
|
|
2537
|
+
manager.adoptAccessToken(newToken);
|
|
2538
|
+
void manager.refresh().catch(() => void 0);
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
const ok = await manager.refresh();
|
|
2542
|
+
if (!ok) {
|
|
2543
|
+
throw new Error("scope switch succeeded server-side but token refresh failed; reload to recover");
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
async function performTenantSwitch(manager, base, tenantId) {
|
|
2547
|
+
const res = await manager.fetch(`${base.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
|
|
2548
|
+
method: "POST",
|
|
2549
|
+
headers: { "Content-Type": "application/json" },
|
|
2550
|
+
body: JSON.stringify({ tenantId })
|
|
2551
|
+
});
|
|
2552
|
+
const body = await res.json().catch(() => ({}));
|
|
2553
|
+
if (!res.ok) {
|
|
2554
|
+
throw new Error(body?.error?.message || `HTTP ${res.status}`);
|
|
2555
|
+
}
|
|
2556
|
+
if (body?.data?.mfaChallengeToken) {
|
|
2557
|
+
return {
|
|
2558
|
+
kind: "mfa_required",
|
|
2559
|
+
tenantId: body?.data?.tenantId || tenantId,
|
|
2560
|
+
mfaChallengeToken: body.data.mfaChallengeToken,
|
|
2561
|
+
availableMethods: Array.isArray(body.data.availableMethods) ? body.data.availableMethods : []
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
if (body?.data?.type === "scope_selection" || body?.data?.scopeSelectionToken) {
|
|
2565
|
+
return {
|
|
2566
|
+
kind: "scope_selection_required",
|
|
2567
|
+
tenantId: body?.data?.tenantId || tenantId,
|
|
2568
|
+
scopeSelectionToken: body?.data?.scopeSelectionToken || "",
|
|
2569
|
+
scopes: Array.isArray(body?.data?.scopes) ? body.data.scopes : []
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
const newToken = body?.data?.accessToken;
|
|
2573
|
+
if (newToken) {
|
|
2574
|
+
manager.adoptAccessToken(newToken);
|
|
2575
|
+
void manager.refresh().catch(() => void 0);
|
|
2576
|
+
return { kind: "ok", tenantId: body?.data?.user?.tenantId || tenantId };
|
|
2577
|
+
}
|
|
2578
|
+
const ok = await manager.refresh();
|
|
2579
|
+
if (!ok) {
|
|
2580
|
+
throw new Error("tenant switch succeeded server-side but token refresh failed; reload to recover");
|
|
2581
|
+
}
|
|
2582
|
+
return { kind: "ok", tenantId };
|
|
2583
|
+
}
|
|
2492
2584
|
function asArray(v) {
|
|
2493
2585
|
if (v == null) return [];
|
|
2494
2586
|
return Array.isArray(v) ? v : [v];
|
|
@@ -2515,7 +2607,14 @@ function claimPermissions(c) {
|
|
|
2515
2607
|
}
|
|
2516
2608
|
return Array.from(out);
|
|
2517
2609
|
}
|
|
2518
|
-
function
|
|
2610
|
+
function claimSatisfiesScope(claims, required) {
|
|
2611
|
+
if (!claims) return false;
|
|
2612
|
+
const ctx = claims.scopeContext;
|
|
2613
|
+
if (!ctx || !ctx.type || !ctx.id) return false;
|
|
2614
|
+
const list = Array.isArray(required) ? required : [required];
|
|
2615
|
+
return list.some((r) => r.type === ctx.type && r.id === ctx.id);
|
|
2616
|
+
}
|
|
2617
|
+
function Protect({ role, permission, scope, condition, fallback = null, children }) {
|
|
2519
2618
|
const { snapshot } = useCtx();
|
|
2520
2619
|
if (snapshot.status !== "authenticated") return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2521
2620
|
const wantedRoles = asArray(role);
|
|
@@ -2528,6 +2627,9 @@ function Protect({ role, permission, condition, fallback = null, children }) {
|
|
|
2528
2627
|
const have = new Set(claimPermissions(snapshot.claims));
|
|
2529
2628
|
if (!wantedPerms.some((p) => have.has(p))) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2530
2629
|
}
|
|
2630
|
+
if (scope) {
|
|
2631
|
+
if (!claimSatisfiesScope(snapshot.claims, scope)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2632
|
+
}
|
|
2531
2633
|
if (condition && !condition(snapshot.claims)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2532
2634
|
return (0, import_react.createElement)(import_react.Fragment, null, children);
|
|
2533
2635
|
}
|
|
@@ -3084,12 +3186,54 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
|
|
|
3084
3186
|
if (!ctx.returnAllowed) return false;
|
|
3085
3187
|
return true;
|
|
3086
3188
|
}
|
|
3189
|
+
function resolveAfterSignInDestination(args) {
|
|
3190
|
+
let raw = args.prop ?? null;
|
|
3191
|
+
if (!raw && typeof args.search === "string") {
|
|
3192
|
+
try {
|
|
3193
|
+
const params = new URLSearchParams(args.search);
|
|
3194
|
+
raw = params.get("return_to") ?? params.get("next");
|
|
3195
|
+
} catch {
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return sanitizeReturnTo(raw, {
|
|
3199
|
+
allowedOrigins: args.allowedOrigins,
|
|
3200
|
+
currentOrigin: args.currentOrigin,
|
|
3201
|
+
fallback: "/"
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3087
3204
|
function SignIn(props) {
|
|
3088
3205
|
const providerCtx = (0, import_react.useContext)(IQAuthContext);
|
|
3089
3206
|
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
|
|
3090
3207
|
const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
|
|
3091
3208
|
const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
|
|
3092
|
-
const
|
|
3209
|
+
const afterSignInUrl = resolveAfterSignInDestination({
|
|
3210
|
+
prop: props.afterSignInUrl,
|
|
3211
|
+
search: typeof window !== "undefined" ? window.location.search : "",
|
|
3212
|
+
allowedOrigins: providerCtx?.allowedReturnOrigins
|
|
3213
|
+
});
|
|
3214
|
+
const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso, scopeHint: scopeHintProp } = props;
|
|
3215
|
+
const effectiveScopeHint = (() => {
|
|
3216
|
+
const fromProp = scopeHintProp ?? null;
|
|
3217
|
+
let raw = fromProp;
|
|
3218
|
+
if (!raw && typeof window !== "undefined") {
|
|
3219
|
+
try {
|
|
3220
|
+
raw = new URLSearchParams(window.location.search).get("scope_hint");
|
|
3221
|
+
} catch {
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
if (!raw) return null;
|
|
3225
|
+
if (typeof raw === "object" && raw && raw.type && raw.id) {
|
|
3226
|
+
const t3 = raw.type;
|
|
3227
|
+
if (t3 === "vendor" || t3 === "source" || t3 === "client") return { type: t3, id: String(raw.id) };
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
if (typeof raw === "string" && raw.includes(":")) {
|
|
3231
|
+
const [t3, id] = raw.split(":", 2);
|
|
3232
|
+
if ((t3 === "vendor" || t3 === "source" || t3 === "client") && id) return { type: t3, id };
|
|
3233
|
+
}
|
|
3234
|
+
return null;
|
|
3235
|
+
})();
|
|
3236
|
+
const scopeHintBody = effectiveScopeHint ? { scopeHint: effectiveScopeHint } : {};
|
|
3093
3237
|
const silentSsoEnabled = instanceSilentSso ?? providerCtx?.silentSso ?? false;
|
|
3094
3238
|
const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
|
|
3095
3239
|
if (!iqAuthBaseUrl || !appKey) {
|
|
@@ -3110,6 +3254,11 @@ function SignIn(props) {
|
|
|
3110
3254
|
appKey
|
|
3111
3255
|
});
|
|
3112
3256
|
if (guardError) throw guardError;
|
|
3257
|
+
(0, import_react.useEffect)(() => {
|
|
3258
|
+
if (typeof document === "undefined") return;
|
|
3259
|
+
const attrs = "; path=/; SameSite=Lax" + (typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "");
|
|
3260
|
+
document.cookie = `iqauth_return_to=${encodeURIComponent(afterSignInUrl)}${attrs}`;
|
|
3261
|
+
}, [afterSignInUrl]);
|
|
3113
3262
|
const preflightLoggedRef = (0, import_react.useRef)(false);
|
|
3114
3263
|
(0, import_react.useEffect)(() => {
|
|
3115
3264
|
if (!ctx || preflightLoggedRef.current) return;
|
|
@@ -3125,6 +3274,7 @@ function SignIn(props) {
|
|
|
3125
3274
|
const [formError, setFormError] = (0, import_react.useState)("");
|
|
3126
3275
|
const [mfa, setMfa] = (0, import_react.useState)(null);
|
|
3127
3276
|
const [tenantSel, setTenantSel] = (0, import_react.useState)(null);
|
|
3277
|
+
const [scopeSel, setScopeSel] = (0, import_react.useState)(null);
|
|
3128
3278
|
const [oauthExchanging, setOauthExchanging] = (0, import_react.useState)(false);
|
|
3129
3279
|
const [silent, setSilent] = (0, import_react.useState)("idle");
|
|
3130
3280
|
const [forcePrompt, setForcePrompt] = (0, import_react.useState)(false);
|
|
@@ -3155,6 +3305,12 @@ function SignIn(props) {
|
|
|
3155
3305
|
setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
|
|
3156
3306
|
return true;
|
|
3157
3307
|
}
|
|
3308
|
+
if (payload.type === "scope_selection") {
|
|
3309
|
+
setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
|
|
3310
|
+
setTenantSel(null);
|
|
3311
|
+
setMfa(null);
|
|
3312
|
+
return true;
|
|
3313
|
+
}
|
|
3158
3314
|
if (payload.type === "mfa_required") {
|
|
3159
3315
|
const methods = payload.availableMethods || ["totp"];
|
|
3160
3316
|
setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
|
|
@@ -3175,7 +3331,7 @@ function SignIn(props) {
|
|
|
3175
3331
|
method: "POST",
|
|
3176
3332
|
headers: { "Content-Type": "application/json" },
|
|
3177
3333
|
credentials: "include",
|
|
3178
|
-
body: JSON.stringify({ email, password, ...oidcPayload() })
|
|
3334
|
+
body: JSON.stringify({ email, password, ...oidcPayload(), ...scopeHintBody })
|
|
3179
3335
|
});
|
|
3180
3336
|
const payload = await r.json().catch(() => ({}));
|
|
3181
3337
|
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
@@ -3198,7 +3354,8 @@ function SignIn(props) {
|
|
|
3198
3354
|
code: mfa.code,
|
|
3199
3355
|
method: mfa.selected,
|
|
3200
3356
|
useBackup: mfa.backup,
|
|
3201
|
-
...oidcPayload()
|
|
3357
|
+
...oidcPayload(),
|
|
3358
|
+
...scopeHintBody
|
|
3202
3359
|
})
|
|
3203
3360
|
});
|
|
3204
3361
|
const payload = await r.json().catch(() => ({}));
|
|
@@ -3213,7 +3370,21 @@ function SignIn(props) {
|
|
|
3213
3370
|
method: "POST",
|
|
3214
3371
|
headers: { "Content-Type": "application/json" },
|
|
3215
3372
|
credentials: "include",
|
|
3216
|
-
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
|
|
3373
|
+
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload(), ...scopeHintBody })
|
|
3374
|
+
});
|
|
3375
|
+
const payload = await r.json().catch(() => ({}));
|
|
3376
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
3377
|
+
setSubmitting(false);
|
|
3378
|
+
};
|
|
3379
|
+
const submitScope = async (membershipId) => {
|
|
3380
|
+
if (!scopeSel) return;
|
|
3381
|
+
setSubmitting(true);
|
|
3382
|
+
setFormError("");
|
|
3383
|
+
const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-scope-select`, {
|
|
3384
|
+
method: "POST",
|
|
3385
|
+
headers: { "Content-Type": "application/json" },
|
|
3386
|
+
credentials: "include",
|
|
3387
|
+
body: JSON.stringify({ scopeSelectionToken: scopeSel.token, membershipId, ...oidcPayload(), ...scopeHintBody })
|
|
3217
3388
|
});
|
|
3218
3389
|
const payload = await r.json().catch(() => ({}));
|
|
3219
3390
|
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
@@ -3281,6 +3452,11 @@ function SignIn(props) {
|
|
|
3281
3452
|
setSilent("failed");
|
|
3282
3453
|
return;
|
|
3283
3454
|
}
|
|
3455
|
+
if (payload?.type === "scope_selection") {
|
|
3456
|
+
setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
|
|
3457
|
+
setSilent("failed");
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3284
3460
|
setSilent("failed");
|
|
3285
3461
|
} catch {
|
|
3286
3462
|
setSilent("failed");
|
|
@@ -3292,6 +3468,7 @@ function SignIn(props) {
|
|
|
3292
3468
|
setForcePrompt(true);
|
|
3293
3469
|
setSilent("skipped");
|
|
3294
3470
|
setTenantSel(null);
|
|
3471
|
+
setScopeSel(null);
|
|
3295
3472
|
if (typeof window !== "undefined") {
|
|
3296
3473
|
try {
|
|
3297
3474
|
const u = new URL(window.location.href);
|
|
@@ -3362,7 +3539,27 @@ function SignIn(props) {
|
|
|
3362
3539
|
]
|
|
3363
3540
|
},
|
|
3364
3541
|
tn.tenantId
|
|
3365
|
-
)) }) :
|
|
3542
|
+
)) }) : scopeSel ? (
|
|
3543
|
+
// Task #171 — scope picker (source/client memberships)
|
|
3544
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-label": t2("signIn.selectScope") || "Choose scope", "data-iqauth-scope-picker": "1", style: { display: "flex", flexDirection: "column", gap: 8 }, children: scopeSel.scopes.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3545
|
+
"button",
|
|
3546
|
+
{
|
|
3547
|
+
type: "button",
|
|
3548
|
+
"data-iqauth-scope": s.membershipId,
|
|
3549
|
+
onClick: () => submitScope(s.membershipId),
|
|
3550
|
+
style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
|
|
3551
|
+
children: [
|
|
3552
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: s.scopeName }),
|
|
3553
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: [
|
|
3554
|
+
s.scopeType,
|
|
3555
|
+
" \xB7 ",
|
|
3556
|
+
s.roleName
|
|
3557
|
+
] })
|
|
3558
|
+
]
|
|
3559
|
+
},
|
|
3560
|
+
s.membershipId
|
|
3561
|
+
)) })
|
|
3562
|
+
) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
|
|
3366
3563
|
!mfa.backup && mfa.methods.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("mfa.title"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { style: inputStyle(), value: mfa.selected, onChange: (e) => setMfa({ ...mfa, selected: e.target.value }), children: mfa.methods.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: m, children: m.toUpperCase() }, m)) }) }) : null,
|
|
3367
3564
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3368
3565
|
"input",
|
|
@@ -3654,9 +3851,12 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
3654
3851
|
const t2 = useT();
|
|
3655
3852
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3656
3853
|
const accent = branding?.accentColor || "#6366f1";
|
|
3854
|
+
const { manager } = useCtx();
|
|
3657
3855
|
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
3658
3856
|
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
3659
3857
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
3858
|
+
const [pendingStep, setPendingStep] = (0, import_react.useState)(null);
|
|
3859
|
+
const [switchError, setSwitchError] = (0, import_react.useState)(null);
|
|
3660
3860
|
(0, import_react.useEffect)(() => {
|
|
3661
3861
|
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
3662
3862
|
if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
|
|
@@ -3666,18 +3866,48 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
3666
3866
|
});
|
|
3667
3867
|
}, [iqAuthBaseUrl]);
|
|
3668
3868
|
const switchTo = async (tenantId) => {
|
|
3869
|
+
setSwitchError(null);
|
|
3870
|
+
setPendingStep(null);
|
|
3669
3871
|
try {
|
|
3670
|
-
await
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3872
|
+
const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
|
|
3873
|
+
if (result.kind === "mfa_required") {
|
|
3874
|
+
setPendingStep({
|
|
3875
|
+
kind: "mfa_required",
|
|
3876
|
+
tenantId: result.tenantId,
|
|
3877
|
+
mfaChallengeToken: result.mfaChallengeToken,
|
|
3878
|
+
availableMethods: result.availableMethods
|
|
3879
|
+
});
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
if (result.kind === "scope_selection_required") {
|
|
3883
|
+
setPendingStep({
|
|
3884
|
+
kind: "scope_selection_required",
|
|
3885
|
+
tenantId: result.tenantId,
|
|
3886
|
+
scopeSelectionToken: result.scopeSelectionToken
|
|
3887
|
+
});
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3675
3890
|
setActiveTenantId(tenantId);
|
|
3676
3891
|
setOpen(false);
|
|
3677
3892
|
onSwitched?.(tenantId);
|
|
3678
|
-
} catch {
|
|
3893
|
+
} catch (err) {
|
|
3894
|
+
setSwitchError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
3679
3895
|
}
|
|
3680
3896
|
};
|
|
3897
|
+
const hostedSignInUrl = () => {
|
|
3898
|
+
const baseHosted = `${iqAuthBaseUrl.replace(/\/$/, "")}/sign-in`;
|
|
3899
|
+
if (!pendingStep) return baseHosted;
|
|
3900
|
+
const params = new URLSearchParams();
|
|
3901
|
+
if (pendingStep.kind === "mfa_required") {
|
|
3902
|
+
params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
|
|
3903
|
+
if (pendingStep.availableMethods.length) {
|
|
3904
|
+
params.set("availableMethods", pendingStep.availableMethods.join(","));
|
|
3905
|
+
}
|
|
3906
|
+
} else {
|
|
3907
|
+
params.set("prompt", "login");
|
|
3908
|
+
}
|
|
3909
|
+
return `${baseHosted}?${params.toString()}`;
|
|
3910
|
+
};
|
|
3681
3911
|
const active = memberships.find((m) => m.tenantId === activeTenantId);
|
|
3682
3912
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3683
3913
|
"div",
|
|
@@ -3698,44 +3928,274 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
3698
3928
|
children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
|
|
3699
3929
|
}
|
|
3700
3930
|
),
|
|
3701
|
-
open ? /* @__PURE__ */ (0, import_jsx_runtime.
|
|
3931
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "menu", style: {
|
|
3702
3932
|
position: "absolute",
|
|
3703
3933
|
left: 0,
|
|
3704
3934
|
top: 36,
|
|
3705
|
-
minWidth:
|
|
3935
|
+
minWidth: 260,
|
|
3706
3936
|
background: "#fff",
|
|
3707
3937
|
border: "1px solid rgba(15,23,42,0.12)",
|
|
3708
3938
|
borderRadius: 8,
|
|
3709
3939
|
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
3710
3940
|
padding: 8,
|
|
3711
3941
|
zIndex: 100
|
|
3712
|
-
}, children:
|
|
3942
|
+
}, children: [
|
|
3943
|
+
memberships.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: t2("orgSwitcher.noOrgs") }) : memberships.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3944
|
+
"button",
|
|
3945
|
+
{
|
|
3946
|
+
role: "menuitem",
|
|
3947
|
+
type: "button",
|
|
3948
|
+
onClick: () => switchTo(m.tenantId),
|
|
3949
|
+
style: {
|
|
3950
|
+
display: "block",
|
|
3951
|
+
width: "100%",
|
|
3952
|
+
textAlign: "left",
|
|
3953
|
+
padding: "8px 10px",
|
|
3954
|
+
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
3955
|
+
border: "none",
|
|
3956
|
+
borderRadius: 4,
|
|
3957
|
+
cursor: "pointer",
|
|
3958
|
+
fontSize: 13,
|
|
3959
|
+
color: "#0f172a"
|
|
3960
|
+
},
|
|
3961
|
+
children: [
|
|
3962
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
3963
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
3964
|
+
]
|
|
3965
|
+
},
|
|
3966
|
+
m.tenantId
|
|
3967
|
+
)),
|
|
3968
|
+
pendingStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3969
|
+
"div",
|
|
3970
|
+
{
|
|
3971
|
+
"data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-switch-mfa-required" : "prompt-org-switch-scope-required",
|
|
3972
|
+
role: "alert",
|
|
3973
|
+
style: {
|
|
3974
|
+
marginTop: 8,
|
|
3975
|
+
padding: "10px 12px",
|
|
3976
|
+
background: `${accent}10`,
|
|
3977
|
+
border: `1px solid ${accent}55`,
|
|
3978
|
+
borderRadius: 6
|
|
3979
|
+
},
|
|
3980
|
+
children: [
|
|
3981
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
|
|
3982
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
|
|
3983
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3984
|
+
"a",
|
|
3985
|
+
{
|
|
3986
|
+
href: hostedSignInUrl(),
|
|
3987
|
+
"data-testid": "link-org-switch-continue-hosted",
|
|
3988
|
+
style: {
|
|
3989
|
+
display: "inline-block",
|
|
3990
|
+
fontSize: 12,
|
|
3991
|
+
fontWeight: 600,
|
|
3992
|
+
color: branding?.accentColor || accent,
|
|
3993
|
+
textDecoration: "none"
|
|
3994
|
+
},
|
|
3995
|
+
children: t2("orgSwitcher.continueInHostedSignIn")
|
|
3996
|
+
}
|
|
3997
|
+
)
|
|
3998
|
+
]
|
|
3999
|
+
}
|
|
4000
|
+
) : null,
|
|
4001
|
+
switchError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-org-switch-error", style: { fontSize: 12, color: "#b91c1c", padding: "6px 8px 0" }, children: switchError }) : null
|
|
4002
|
+
] }) : null
|
|
4003
|
+
]
|
|
4004
|
+
}
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
function useMemberships() {
|
|
4008
|
+
const { manager, snapshot } = useCtx();
|
|
4009
|
+
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
4010
|
+
const [isLoading, setIsLoading] = (0, import_react.useState)(true);
|
|
4011
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4012
|
+
const base = manager.issuerUrl.replace(/\/$/, "");
|
|
4013
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
4014
|
+
setIsLoading(true);
|
|
4015
|
+
setError(null);
|
|
4016
|
+
try {
|
|
4017
|
+
const res = await manager.fetch(`${base}/api/v1/auth/available-scopes`);
|
|
4018
|
+
const json = await res.json().catch(() => ({}));
|
|
4019
|
+
if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
|
|
4020
|
+
const tree = json.data || {};
|
|
4021
|
+
const flat = [];
|
|
4022
|
+
for (const v of tree.vendors || []) {
|
|
4023
|
+
flat.push({
|
|
4024
|
+
membershipId: v.membershipId,
|
|
4025
|
+
scopeType: "vendor",
|
|
4026
|
+
scopeId: v.id,
|
|
4027
|
+
scopeName: v.name,
|
|
4028
|
+
role: v.role,
|
|
4029
|
+
grantedVia: v.grantedVia || "direct"
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
for (const s of tree.sources || []) {
|
|
4033
|
+
flat.push({
|
|
4034
|
+
membershipId: s.membershipId,
|
|
4035
|
+
scopeType: "source",
|
|
4036
|
+
scopeId: s.id,
|
|
4037
|
+
scopeName: s.name,
|
|
4038
|
+
role: s.role,
|
|
4039
|
+
grantedVia: s.grantedVia || "direct"
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
for (const c of tree.clients || []) {
|
|
4043
|
+
flat.push({
|
|
4044
|
+
membershipId: c.membershipId,
|
|
4045
|
+
scopeType: "client",
|
|
4046
|
+
scopeId: c.id,
|
|
4047
|
+
scopeName: c.name,
|
|
4048
|
+
role: c.role,
|
|
4049
|
+
grantedVia: c.grantedVia || "direct"
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
setMemberships(flat);
|
|
4053
|
+
} catch (err) {
|
|
4054
|
+
setError(err.message);
|
|
4055
|
+
setMemberships([]);
|
|
4056
|
+
} finally {
|
|
4057
|
+
setIsLoading(false);
|
|
4058
|
+
}
|
|
4059
|
+
}, [manager, base]);
|
|
4060
|
+
(0, import_react.useEffect)(() => {
|
|
4061
|
+
if (snapshot.status !== "authenticated") {
|
|
4062
|
+
setMemberships([]);
|
|
4063
|
+
setIsLoading(false);
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
void refresh();
|
|
4067
|
+
}, [snapshot.status, snapshot.tenantId, refresh]);
|
|
4068
|
+
const switchScope = (0, import_react.useCallback)(
|
|
4069
|
+
(target) => performScopeSwitch(manager, base, target),
|
|
4070
|
+
[manager, base]
|
|
4071
|
+
);
|
|
4072
|
+
const active = (0, import_react.useMemo)(() => {
|
|
4073
|
+
const ctx = snapshot.user?.scopeContext;
|
|
4074
|
+
if (!ctx) return null;
|
|
4075
|
+
return {
|
|
4076
|
+
type: ctx.type,
|
|
4077
|
+
id: ctx.id,
|
|
4078
|
+
role: ctx.role,
|
|
4079
|
+
membershipId: ctx.membershipId
|
|
4080
|
+
};
|
|
4081
|
+
}, [snapshot.user, snapshot.version]);
|
|
4082
|
+
return { isLoading, error, memberships, active, refresh, switchScope };
|
|
4083
|
+
}
|
|
4084
|
+
function ScopeSwitcher({ onSwitched, include, className }) {
|
|
4085
|
+
const { memberships, active, isLoading, error, switchScope } = useMemberships();
|
|
4086
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
4087
|
+
const [busy, setBusy] = (0, import_react.useState)(null);
|
|
4088
|
+
const [switchError, setSwitchError] = (0, import_react.useState)(null);
|
|
4089
|
+
const visible = (0, import_react.useMemo)(
|
|
4090
|
+
() => include ? memberships.filter((m) => include.includes(m.scopeType)) : memberships,
|
|
4091
|
+
[memberships, include]
|
|
4092
|
+
);
|
|
4093
|
+
if (isLoading) {
|
|
4094
|
+
return (0, import_react.createElement)(
|
|
4095
|
+
"div",
|
|
4096
|
+
{ className, "data-testid": "scope-switcher-loading", style: { fontSize: 13, opacity: 0.6 } },
|
|
4097
|
+
"Loading scopes\u2026"
|
|
4098
|
+
);
|
|
4099
|
+
}
|
|
4100
|
+
if (error) {
|
|
4101
|
+
return (0, import_react.createElement)(
|
|
4102
|
+
"div",
|
|
4103
|
+
{ className, "data-testid": "scope-switcher-error", style: { fontSize: 13, color: "#b91c1c" } },
|
|
4104
|
+
error
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
if (!visible.length) return null;
|
|
4108
|
+
const activeLabel = active ? visible.find((m) => m.scopeType === active.type && m.scopeId === active.id)?.scopeName || `${active.type}:${active.id}` : "Select a scope";
|
|
4109
|
+
return (0, import_react.createElement)(
|
|
4110
|
+
"div",
|
|
4111
|
+
{ className, "data-testid": "scope-switcher", style: { position: "relative", display: "inline-block" } },
|
|
4112
|
+
(0, import_react.createElement)(
|
|
4113
|
+
"button",
|
|
4114
|
+
{
|
|
4115
|
+
type: "button",
|
|
4116
|
+
"data-testid": "scope-switcher-trigger",
|
|
4117
|
+
onClick: () => setOpen((v) => !v),
|
|
4118
|
+
style: {
|
|
4119
|
+
padding: "6px 10px",
|
|
4120
|
+
border: "1px solid #e5e7eb",
|
|
4121
|
+
borderRadius: 6,
|
|
4122
|
+
background: "#fff",
|
|
4123
|
+
cursor: "pointer",
|
|
4124
|
+
fontSize: 13
|
|
4125
|
+
}
|
|
4126
|
+
},
|
|
4127
|
+
active ? `${active.type}: ${activeLabel}` : activeLabel
|
|
4128
|
+
),
|
|
4129
|
+
switchError ? (0, import_react.createElement)(
|
|
4130
|
+
"div",
|
|
4131
|
+
{
|
|
4132
|
+
"data-testid": "scope-switcher-switch-error",
|
|
4133
|
+
style: { marginTop: 4, fontSize: 12, color: "#b91c1c" }
|
|
4134
|
+
},
|
|
4135
|
+
switchError
|
|
4136
|
+
) : null,
|
|
4137
|
+
open ? (0, import_react.createElement)(
|
|
4138
|
+
"div",
|
|
4139
|
+
{
|
|
4140
|
+
"data-testid": "scope-switcher-menu",
|
|
4141
|
+
style: {
|
|
4142
|
+
position: "absolute",
|
|
4143
|
+
top: "100%",
|
|
4144
|
+
left: 0,
|
|
4145
|
+
marginTop: 4,
|
|
4146
|
+
minWidth: 220,
|
|
4147
|
+
background: "#fff",
|
|
4148
|
+
border: "1px solid #e5e7eb",
|
|
4149
|
+
borderRadius: 6,
|
|
4150
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
4151
|
+
padding: 4,
|
|
4152
|
+
zIndex: 50
|
|
4153
|
+
}
|
|
4154
|
+
},
|
|
4155
|
+
visible.map((m) => {
|
|
4156
|
+
const isActive = active?.type === m.scopeType && active?.id === m.scopeId;
|
|
4157
|
+
const key = `${m.scopeType}:${m.scopeId}`;
|
|
4158
|
+
return (0, import_react.createElement)(
|
|
3713
4159
|
"button",
|
|
3714
4160
|
{
|
|
3715
|
-
|
|
4161
|
+
key,
|
|
3716
4162
|
type: "button",
|
|
3717
|
-
|
|
4163
|
+
"data-testid": `scope-switcher-option-${m.scopeType}-${m.scopeId}`,
|
|
4164
|
+
disabled: busy === key,
|
|
4165
|
+
onClick: async () => {
|
|
4166
|
+
setBusy(key);
|
|
4167
|
+
setSwitchError(null);
|
|
4168
|
+
try {
|
|
4169
|
+
await switchScope({ type: m.scopeType, id: m.scopeId });
|
|
4170
|
+
setOpen(false);
|
|
4171
|
+
onSwitched?.({ type: m.scopeType, id: m.scopeId });
|
|
4172
|
+
} catch (err) {
|
|
4173
|
+
setSwitchError(err.message || "Scope switch failed");
|
|
4174
|
+
} finally {
|
|
4175
|
+
setBusy(null);
|
|
4176
|
+
}
|
|
4177
|
+
},
|
|
3718
4178
|
style: {
|
|
3719
4179
|
display: "block",
|
|
3720
4180
|
width: "100%",
|
|
3721
4181
|
textAlign: "left",
|
|
3722
4182
|
padding: "8px 10px",
|
|
3723
|
-
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
3724
4183
|
border: "none",
|
|
4184
|
+
background: isActive ? "#f3f4f6" : "transparent",
|
|
4185
|
+
cursor: busy ? "wait" : "pointer",
|
|
3725
4186
|
borderRadius: 4,
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
color: "#0f172a"
|
|
3729
|
-
},
|
|
3730
|
-
children: [
|
|
3731
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
3732
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
3733
|
-
]
|
|
4187
|
+
fontSize: 13
|
|
4188
|
+
}
|
|
3734
4189
|
},
|
|
3735
|
-
m.
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
4190
|
+
(0, import_react.createElement)("div", { style: { fontWeight: 500 } }, m.scopeName),
|
|
4191
|
+
(0, import_react.createElement)(
|
|
4192
|
+
"div",
|
|
4193
|
+
{ style: { fontSize: 11, opacity: 0.6 } },
|
|
4194
|
+
`${m.scopeType} \xB7 ${m.role}${m.grantedVia && m.grantedVia !== "direct" ? ` \xB7 via ${m.grantedVia}` : ""}`
|
|
4195
|
+
)
|
|
4196
|
+
);
|
|
4197
|
+
})
|
|
4198
|
+
) : null
|
|
3739
4199
|
);
|
|
3740
4200
|
}
|
|
3741
4201
|
function useImpersonation() {
|
|
@@ -4348,10 +4808,13 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
4348
4808
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
4349
4809
|
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
4350
4810
|
const accent = branding?.accentColor || "#6366f1";
|
|
4811
|
+
const t2 = useT();
|
|
4812
|
+
const { manager } = useCtx();
|
|
4351
4813
|
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
4352
4814
|
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
4353
4815
|
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
4354
4816
|
const [error, setError] = (0, import_react.useState)(null);
|
|
4817
|
+
const [pendingStep, setPendingStep] = (0, import_react.useState)(null);
|
|
4355
4818
|
(0, import_react.useEffect)(() => {
|
|
4356
4819
|
let cancelled = false;
|
|
4357
4820
|
Promise.all([
|
|
@@ -4375,21 +4838,81 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
4375
4838
|
onSelect?.(tenantId);
|
|
4376
4839
|
return;
|
|
4377
4840
|
}
|
|
4841
|
+
setError(null);
|
|
4842
|
+
setPendingStep(null);
|
|
4378
4843
|
try {
|
|
4379
|
-
await
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4844
|
+
const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
|
|
4845
|
+
if (result.kind === "mfa_required") {
|
|
4846
|
+
setPendingStep({
|
|
4847
|
+
kind: "mfa_required",
|
|
4848
|
+
tenantId: result.tenantId,
|
|
4849
|
+
mfaChallengeToken: result.mfaChallengeToken,
|
|
4850
|
+
availableMethods: result.availableMethods
|
|
4851
|
+
});
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
if (result.kind === "scope_selection_required") {
|
|
4855
|
+
setPendingStep({
|
|
4856
|
+
kind: "scope_selection_required",
|
|
4857
|
+
tenantId: result.tenantId,
|
|
4858
|
+
scopeSelectionToken: result.scopeSelectionToken
|
|
4859
|
+
});
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4385
4862
|
setActiveTenantId(tenantId);
|
|
4386
4863
|
onSelect?.(tenantId);
|
|
4387
4864
|
} catch (err) {
|
|
4388
4865
|
setError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
4389
4866
|
}
|
|
4390
4867
|
};
|
|
4868
|
+
const hostedSignInUrl = () => {
|
|
4869
|
+
const baseHosted = `${baseUrl}/sign-in`;
|
|
4870
|
+
if (!pendingStep) return baseHosted;
|
|
4871
|
+
const params = new URLSearchParams();
|
|
4872
|
+
if (pendingStep.kind === "mfa_required") {
|
|
4873
|
+
params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
|
|
4874
|
+
if (pendingStep.availableMethods.length) {
|
|
4875
|
+
params.set("availableMethods", pendingStep.availableMethods.join(","));
|
|
4876
|
+
}
|
|
4877
|
+
} else {
|
|
4878
|
+
params.set("prompt", "login");
|
|
4879
|
+
}
|
|
4880
|
+
return `${baseHosted}?${params.toString()}`;
|
|
4881
|
+
};
|
|
4391
4882
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: "Your organizations", subtitle: "Select an organization to make it active.", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-iqauth-sdk-org-list": "", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
4392
4883
|
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
4884
|
+
pendingStep ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
4885
|
+
"div",
|
|
4886
|
+
{
|
|
4887
|
+
"data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-list-mfa-required" : "prompt-org-list-scope-required",
|
|
4888
|
+
role: "alert",
|
|
4889
|
+
style: {
|
|
4890
|
+
padding: "10px 12px",
|
|
4891
|
+
background: `${accent}10`,
|
|
4892
|
+
border: `1px solid ${accent}55`,
|
|
4893
|
+
borderRadius: 6
|
|
4894
|
+
},
|
|
4895
|
+
children: [
|
|
4896
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
|
|
4897
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
|
|
4898
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4899
|
+
"a",
|
|
4900
|
+
{
|
|
4901
|
+
href: hostedSignInUrl(),
|
|
4902
|
+
"data-testid": "link-org-list-continue-hosted",
|
|
4903
|
+
style: {
|
|
4904
|
+
display: "inline-block",
|
|
4905
|
+
fontSize: 12,
|
|
4906
|
+
fontWeight: 600,
|
|
4907
|
+
color: branding?.accentColor || accent,
|
|
4908
|
+
textDecoration: "none"
|
|
4909
|
+
},
|
|
4910
|
+
children: t2("orgSwitcher.continueInHostedSignIn")
|
|
4911
|
+
}
|
|
4912
|
+
)
|
|
4913
|
+
]
|
|
4914
|
+
}
|
|
4915
|
+
) : null,
|
|
4393
4916
|
loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13 }, children: "Loading\u2026" }) : memberships.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6 }, children: "You don\u2019t belong to any organizations yet." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: memberships.map((m) => {
|
|
4394
4917
|
const active = m.tenantId === activeTenantId;
|
|
4395
4918
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -4427,8 +4950,8 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
4427
4950
|
iqAuthBaseUrl,
|
|
4428
4951
|
unstyled: true,
|
|
4429
4952
|
appearance,
|
|
4430
|
-
onCreated: (
|
|
4431
|
-
onSelect?.(
|
|
4953
|
+
onCreated: (t3) => {
|
|
4954
|
+
onSelect?.(t3.id);
|
|
4432
4955
|
reloadList();
|
|
4433
4956
|
},
|
|
4434
4957
|
redirectUrl: createRedirectUrl
|
|
@@ -4681,6 +5204,7 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4681
5204
|
Protect,
|
|
4682
5205
|
RedirectToSignIn,
|
|
4683
5206
|
RedirectToSignedIn,
|
|
5207
|
+
ScopeSwitcher,
|
|
4684
5208
|
SignIn,
|
|
4685
5209
|
SignUp,
|
|
4686
5210
|
SignedIn,
|
|
@@ -4690,9 +5214,13 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4690
5214
|
Waitlist,
|
|
4691
5215
|
__useIQAuthInternal,
|
|
4692
5216
|
__version__,
|
|
5217
|
+
claimSatisfiesScope,
|
|
4693
5218
|
isReturnToAllowed,
|
|
4694
5219
|
isSilentSsoEligible,
|
|
5220
|
+
performScopeSwitch,
|
|
5221
|
+
performTenantSwitch,
|
|
4695
5222
|
preflightReturnTo,
|
|
5223
|
+
resolveAfterSignInDestination,
|
|
4696
5224
|
revokeSession,
|
|
4697
5225
|
sanitizeBrandCss,
|
|
4698
5226
|
sanitizeReturnTo,
|
|
@@ -4706,6 +5234,7 @@ var __version__ = "phase-bc-1.0.0";
|
|
|
4706
5234
|
useLinkedIdentities,
|
|
4707
5235
|
useLocale,
|
|
4708
5236
|
useMagicLink,
|
|
5237
|
+
useMemberships,
|
|
4709
5238
|
useOrganization,
|
|
4710
5239
|
usePasskey,
|
|
4711
5240
|
useResolvedSdkBranding,
|