@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
|
@@ -9,60 +9,22 @@ import {
|
|
|
9
9
|
requestMagicLink,
|
|
10
10
|
signInWithPasskey,
|
|
11
11
|
unlinkProvider
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4V7FKOTG.mjs";
|
|
13
13
|
import {
|
|
14
14
|
handleAuthCallback,
|
|
15
15
|
redirectToSignIn,
|
|
16
16
|
signIn,
|
|
17
17
|
signOut
|
|
18
18
|
} from "./chunk-GN37E64I.mjs";
|
|
19
|
+
import {
|
|
20
|
+
sanitizeReturnTo
|
|
21
|
+
} from "./chunk-JRDVUWAL.mjs";
|
|
19
22
|
import {
|
|
20
23
|
defaultBundle,
|
|
21
24
|
localizeErrorCode,
|
|
22
25
|
resolveBundle,
|
|
23
26
|
t
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
|
|
26
|
-
// src/browser/returnTo.ts
|
|
27
|
-
function normalizeOrigin(o) {
|
|
28
|
-
try {
|
|
29
|
-
return new URL(o).origin;
|
|
30
|
-
} catch {
|
|
31
|
-
return o.replace(/\/+$/, "");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function sanitizeReturnTo(input, options = {}) {
|
|
35
|
-
const fallback = options.fallback ?? "/";
|
|
36
|
-
if (!input || typeof input !== "string") return fallback;
|
|
37
|
-
const trimmed = input.trim();
|
|
38
|
-
if (!trimmed) return fallback;
|
|
39
|
-
if (trimmed.startsWith("//")) return fallback;
|
|
40
|
-
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
43
|
-
if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
|
|
44
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
45
|
-
}
|
|
46
|
-
let parsed;
|
|
47
|
-
try {
|
|
48
|
-
parsed = new URL(trimmed);
|
|
49
|
-
} catch {
|
|
50
|
-
return fallback;
|
|
51
|
-
}
|
|
52
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
|
|
53
|
-
const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
54
|
-
const allowed = /* @__PURE__ */ new Set();
|
|
55
|
-
if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
|
|
56
|
-
for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
|
|
57
|
-
if (allowed.has(parsed.origin)) return parsed.toString();
|
|
58
|
-
return fallback;
|
|
59
|
-
}
|
|
60
|
-
function isReturnToAllowed(input, options = {}) {
|
|
61
|
-
const fallback = options.fallback ?? "/";
|
|
62
|
-
const out = sanitizeReturnTo(input, options);
|
|
63
|
-
if (!input) return false;
|
|
64
|
-
return out !== fallback || input === fallback;
|
|
65
|
-
}
|
|
27
|
+
} from "./chunk-TLET552H.mjs";
|
|
66
28
|
|
|
67
29
|
// src/react/index.tsx
|
|
68
30
|
import {
|
|
@@ -495,6 +457,65 @@ function RedirectToSignIn(props = {}) {
|
|
|
495
457
|
}
|
|
496
458
|
return null;
|
|
497
459
|
}
|
|
460
|
+
async function performScopeSwitch(manager, base, target) {
|
|
461
|
+
const res = await manager.fetch(`${base}/api/v1/auth/switch-scope`, {
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers: { "Content-Type": "application/json" },
|
|
464
|
+
body: JSON.stringify({ scopeType: target.type, scopeId: target.id })
|
|
465
|
+
});
|
|
466
|
+
const body = await res.json().catch(() => ({}));
|
|
467
|
+
if (!res.ok) {
|
|
468
|
+
throw new Error(body?.error?.message || `HTTP ${res.status}`);
|
|
469
|
+
}
|
|
470
|
+
const newToken = body?.data?.accessToken;
|
|
471
|
+
if (newToken) {
|
|
472
|
+
manager.adoptAccessToken(newToken);
|
|
473
|
+
void manager.refresh().catch(() => void 0);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const ok = await manager.refresh();
|
|
477
|
+
if (!ok) {
|
|
478
|
+
throw new Error("scope switch succeeded server-side but token refresh failed; reload to recover");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function performTenantSwitch(manager, base, tenantId) {
|
|
482
|
+
const res = await manager.fetch(`${base.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Content-Type": "application/json" },
|
|
485
|
+
body: JSON.stringify({ tenantId })
|
|
486
|
+
});
|
|
487
|
+
const body = await res.json().catch(() => ({}));
|
|
488
|
+
if (!res.ok) {
|
|
489
|
+
throw new Error(body?.error?.message || `HTTP ${res.status}`);
|
|
490
|
+
}
|
|
491
|
+
if (body?.data?.mfaChallengeToken) {
|
|
492
|
+
return {
|
|
493
|
+
kind: "mfa_required",
|
|
494
|
+
tenantId: body?.data?.tenantId || tenantId,
|
|
495
|
+
mfaChallengeToken: body.data.mfaChallengeToken,
|
|
496
|
+
availableMethods: Array.isArray(body.data.availableMethods) ? body.data.availableMethods : []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (body?.data?.type === "scope_selection" || body?.data?.scopeSelectionToken) {
|
|
500
|
+
return {
|
|
501
|
+
kind: "scope_selection_required",
|
|
502
|
+
tenantId: body?.data?.tenantId || tenantId,
|
|
503
|
+
scopeSelectionToken: body?.data?.scopeSelectionToken || "",
|
|
504
|
+
scopes: Array.isArray(body?.data?.scopes) ? body.data.scopes : []
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const newToken = body?.data?.accessToken;
|
|
508
|
+
if (newToken) {
|
|
509
|
+
manager.adoptAccessToken(newToken);
|
|
510
|
+
void manager.refresh().catch(() => void 0);
|
|
511
|
+
return { kind: "ok", tenantId: body?.data?.user?.tenantId || tenantId };
|
|
512
|
+
}
|
|
513
|
+
const ok = await manager.refresh();
|
|
514
|
+
if (!ok) {
|
|
515
|
+
throw new Error("tenant switch succeeded server-side but token refresh failed; reload to recover");
|
|
516
|
+
}
|
|
517
|
+
return { kind: "ok", tenantId };
|
|
518
|
+
}
|
|
498
519
|
function asArray(v) {
|
|
499
520
|
if (v == null) return [];
|
|
500
521
|
return Array.isArray(v) ? v : [v];
|
|
@@ -521,7 +542,14 @@ function claimPermissions(c) {
|
|
|
521
542
|
}
|
|
522
543
|
return Array.from(out);
|
|
523
544
|
}
|
|
524
|
-
function
|
|
545
|
+
function claimSatisfiesScope(claims, required) {
|
|
546
|
+
if (!claims) return false;
|
|
547
|
+
const ctx = claims.scopeContext;
|
|
548
|
+
if (!ctx || !ctx.type || !ctx.id) return false;
|
|
549
|
+
const list = Array.isArray(required) ? required : [required];
|
|
550
|
+
return list.some((r) => r.type === ctx.type && r.id === ctx.id);
|
|
551
|
+
}
|
|
552
|
+
function Protect({ role, permission, scope, condition, fallback = null, children }) {
|
|
525
553
|
const { snapshot } = useCtx();
|
|
526
554
|
if (snapshot.status !== "authenticated") return createElement(Fragment, null, fallback);
|
|
527
555
|
const wantedRoles = asArray(role);
|
|
@@ -534,6 +562,9 @@ function Protect({ role, permission, condition, fallback = null, children }) {
|
|
|
534
562
|
const have = new Set(claimPermissions(snapshot.claims));
|
|
535
563
|
if (!wantedPerms.some((p) => have.has(p))) return createElement(Fragment, null, fallback);
|
|
536
564
|
}
|
|
565
|
+
if (scope) {
|
|
566
|
+
if (!claimSatisfiesScope(snapshot.claims, scope)) return createElement(Fragment, null, fallback);
|
|
567
|
+
}
|
|
537
568
|
if (condition && !condition(snapshot.claims)) return createElement(Fragment, null, fallback);
|
|
538
569
|
return createElement(Fragment, null, children);
|
|
539
570
|
}
|
|
@@ -1090,12 +1121,54 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
|
|
|
1090
1121
|
if (!ctx.returnAllowed) return false;
|
|
1091
1122
|
return true;
|
|
1092
1123
|
}
|
|
1124
|
+
function resolveAfterSignInDestination(args) {
|
|
1125
|
+
let raw = args.prop ?? null;
|
|
1126
|
+
if (!raw && typeof args.search === "string") {
|
|
1127
|
+
try {
|
|
1128
|
+
const params = new URLSearchParams(args.search);
|
|
1129
|
+
raw = params.get("return_to") ?? params.get("next");
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return sanitizeReturnTo(raw, {
|
|
1134
|
+
allowedOrigins: args.allowedOrigins,
|
|
1135
|
+
currentOrigin: args.currentOrigin,
|
|
1136
|
+
fallback: "/"
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1093
1139
|
function SignIn(props) {
|
|
1094
1140
|
const providerCtx = useContext(IQAuthContext);
|
|
1095
1141
|
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.hostedIssuerUrl ?? "";
|
|
1096
1142
|
const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
|
|
1097
1143
|
const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
|
|
1098
|
-
const
|
|
1144
|
+
const afterSignInUrl = resolveAfterSignInDestination({
|
|
1145
|
+
prop: props.afterSignInUrl,
|
|
1146
|
+
search: typeof window !== "undefined" ? window.location.search : "",
|
|
1147
|
+
allowedOrigins: providerCtx?.allowedReturnOrigins
|
|
1148
|
+
});
|
|
1149
|
+
const { onRedirect, className, prompt, appearance: instanceAppearance, silentSso: instanceSilentSso, scopeHint: scopeHintProp } = props;
|
|
1150
|
+
const effectiveScopeHint = (() => {
|
|
1151
|
+
const fromProp = scopeHintProp ?? null;
|
|
1152
|
+
let raw = fromProp;
|
|
1153
|
+
if (!raw && typeof window !== "undefined") {
|
|
1154
|
+
try {
|
|
1155
|
+
raw = new URLSearchParams(window.location.search).get("scope_hint");
|
|
1156
|
+
} catch {
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (!raw) return null;
|
|
1160
|
+
if (typeof raw === "object" && raw && raw.type && raw.id) {
|
|
1161
|
+
const t3 = raw.type;
|
|
1162
|
+
if (t3 === "vendor" || t3 === "source" || t3 === "client") return { type: t3, id: String(raw.id) };
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof raw === "string" && raw.includes(":")) {
|
|
1166
|
+
const [t3, id] = raw.split(":", 2);
|
|
1167
|
+
if ((t3 === "vendor" || t3 === "source" || t3 === "client") && id) return { type: t3, id };
|
|
1168
|
+
}
|
|
1169
|
+
return null;
|
|
1170
|
+
})();
|
|
1171
|
+
const scopeHintBody = effectiveScopeHint ? { scopeHint: effectiveScopeHint } : {};
|
|
1099
1172
|
const silentSsoEnabled = instanceSilentSso ?? providerCtx?.silentSso ?? false;
|
|
1100
1173
|
const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
|
|
1101
1174
|
if (!iqAuthBaseUrl || !appKey) {
|
|
@@ -1116,6 +1189,11 @@ function SignIn(props) {
|
|
|
1116
1189
|
appKey
|
|
1117
1190
|
});
|
|
1118
1191
|
if (guardError) throw guardError;
|
|
1192
|
+
useEffect(() => {
|
|
1193
|
+
if (typeof document === "undefined") return;
|
|
1194
|
+
const attrs = "; path=/; SameSite=Lax" + (typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "");
|
|
1195
|
+
document.cookie = `iqauth_return_to=${encodeURIComponent(afterSignInUrl)}${attrs}`;
|
|
1196
|
+
}, [afterSignInUrl]);
|
|
1119
1197
|
const preflightLoggedRef = useRef(false);
|
|
1120
1198
|
useEffect(() => {
|
|
1121
1199
|
if (!ctx || preflightLoggedRef.current) return;
|
|
@@ -1131,6 +1209,7 @@ function SignIn(props) {
|
|
|
1131
1209
|
const [formError, setFormError] = useState("");
|
|
1132
1210
|
const [mfa, setMfa] = useState(null);
|
|
1133
1211
|
const [tenantSel, setTenantSel] = useState(null);
|
|
1212
|
+
const [scopeSel, setScopeSel] = useState(null);
|
|
1134
1213
|
const [oauthExchanging, setOauthExchanging] = useState(false);
|
|
1135
1214
|
const [silent, setSilent] = useState("idle");
|
|
1136
1215
|
const [forcePrompt, setForcePrompt] = useState(false);
|
|
@@ -1161,6 +1240,12 @@ function SignIn(props) {
|
|
|
1161
1240
|
setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
|
|
1162
1241
|
return true;
|
|
1163
1242
|
}
|
|
1243
|
+
if (payload.type === "scope_selection") {
|
|
1244
|
+
setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
|
|
1245
|
+
setTenantSel(null);
|
|
1246
|
+
setMfa(null);
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1164
1249
|
if (payload.type === "mfa_required") {
|
|
1165
1250
|
const methods = payload.availableMethods || ["totp"];
|
|
1166
1251
|
setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
|
|
@@ -1181,7 +1266,7 @@ function SignIn(props) {
|
|
|
1181
1266
|
method: "POST",
|
|
1182
1267
|
headers: { "Content-Type": "application/json" },
|
|
1183
1268
|
credentials: "include",
|
|
1184
|
-
body: JSON.stringify({ email, password, ...oidcPayload() })
|
|
1269
|
+
body: JSON.stringify({ email, password, ...oidcPayload(), ...scopeHintBody })
|
|
1185
1270
|
});
|
|
1186
1271
|
const payload = await r.json().catch(() => ({}));
|
|
1187
1272
|
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
@@ -1204,7 +1289,8 @@ function SignIn(props) {
|
|
|
1204
1289
|
code: mfa.code,
|
|
1205
1290
|
method: mfa.selected,
|
|
1206
1291
|
useBackup: mfa.backup,
|
|
1207
|
-
...oidcPayload()
|
|
1292
|
+
...oidcPayload(),
|
|
1293
|
+
...scopeHintBody
|
|
1208
1294
|
})
|
|
1209
1295
|
});
|
|
1210
1296
|
const payload = await r.json().catch(() => ({}));
|
|
@@ -1219,7 +1305,21 @@ function SignIn(props) {
|
|
|
1219
1305
|
method: "POST",
|
|
1220
1306
|
headers: { "Content-Type": "application/json" },
|
|
1221
1307
|
credentials: "include",
|
|
1222
|
-
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
|
|
1308
|
+
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload(), ...scopeHintBody })
|
|
1309
|
+
});
|
|
1310
|
+
const payload = await r.json().catch(() => ({}));
|
|
1311
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1312
|
+
setSubmitting(false);
|
|
1313
|
+
};
|
|
1314
|
+
const submitScope = async (membershipId) => {
|
|
1315
|
+
if (!scopeSel) return;
|
|
1316
|
+
setSubmitting(true);
|
|
1317
|
+
setFormError("");
|
|
1318
|
+
const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-scope-select`, {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
headers: { "Content-Type": "application/json" },
|
|
1321
|
+
credentials: "include",
|
|
1322
|
+
body: JSON.stringify({ scopeSelectionToken: scopeSel.token, membershipId, ...oidcPayload(), ...scopeHintBody })
|
|
1223
1323
|
});
|
|
1224
1324
|
const payload = await r.json().catch(() => ({}));
|
|
1225
1325
|
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
@@ -1287,6 +1387,11 @@ function SignIn(props) {
|
|
|
1287
1387
|
setSilent("failed");
|
|
1288
1388
|
return;
|
|
1289
1389
|
}
|
|
1390
|
+
if (payload?.type === "scope_selection") {
|
|
1391
|
+
setScopeSel({ token: payload.scopeSelectionToken, tenantId: payload.tenantId, scopes: payload.scopes || [] });
|
|
1392
|
+
setSilent("failed");
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1290
1395
|
setSilent("failed");
|
|
1291
1396
|
} catch {
|
|
1292
1397
|
setSilent("failed");
|
|
@@ -1298,6 +1403,7 @@ function SignIn(props) {
|
|
|
1298
1403
|
setForcePrompt(true);
|
|
1299
1404
|
setSilent("skipped");
|
|
1300
1405
|
setTenantSel(null);
|
|
1406
|
+
setScopeSel(null);
|
|
1301
1407
|
if (typeof window !== "undefined") {
|
|
1302
1408
|
try {
|
|
1303
1409
|
const u = new URL(window.location.href);
|
|
@@ -1368,7 +1474,27 @@ function SignIn(props) {
|
|
|
1368
1474
|
]
|
|
1369
1475
|
},
|
|
1370
1476
|
tn.tenantId
|
|
1371
|
-
)) }) :
|
|
1477
|
+
)) }) : scopeSel ? (
|
|
1478
|
+
// Task #171 — scope picker (source/client memberships)
|
|
1479
|
+
/* @__PURE__ */ 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__ */ jsxs(
|
|
1480
|
+
"button",
|
|
1481
|
+
{
|
|
1482
|
+
type: "button",
|
|
1483
|
+
"data-iqauth-scope": s.membershipId,
|
|
1484
|
+
onClick: () => submitScope(s.membershipId),
|
|
1485
|
+
style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
|
|
1486
|
+
children: [
|
|
1487
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontWeight: 500 }, children: s.scopeName }),
|
|
1488
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: [
|
|
1489
|
+
s.scopeType,
|
|
1490
|
+
" \xB7 ",
|
|
1491
|
+
s.roleName
|
|
1492
|
+
] })
|
|
1493
|
+
]
|
|
1494
|
+
},
|
|
1495
|
+
s.membershipId
|
|
1496
|
+
)) })
|
|
1497
|
+
) : mfa ? /* @__PURE__ */ jsxs("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
|
|
1372
1498
|
!mfa.backup && mfa.methods.length > 1 ? /* @__PURE__ */ jsx(Field, { label: t2("mfa.title"), children: /* @__PURE__ */ jsx("select", { style: inputStyle(), value: mfa.selected, onChange: (e) => setMfa({ ...mfa, selected: e.target.value }), children: mfa.methods.map((m) => /* @__PURE__ */ jsx("option", { value: m, children: m.toUpperCase() }, m)) }) }) : null,
|
|
1373
1499
|
/* @__PURE__ */ jsx(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ jsx(
|
|
1374
1500
|
"input",
|
|
@@ -1660,9 +1786,12 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
1660
1786
|
const t2 = useT();
|
|
1661
1787
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
1662
1788
|
const accent = branding?.accentColor || "#6366f1";
|
|
1789
|
+
const { manager } = useCtx();
|
|
1663
1790
|
const [memberships, setMemberships] = useState([]);
|
|
1664
1791
|
const [activeTenantId, setActiveTenantId] = useState(null);
|
|
1665
1792
|
const [open, setOpen] = useState(false);
|
|
1793
|
+
const [pendingStep, setPendingStep] = useState(null);
|
|
1794
|
+
const [switchError, setSwitchError] = useState(null);
|
|
1666
1795
|
useEffect(() => {
|
|
1667
1796
|
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
1668
1797
|
if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
|
|
@@ -1672,17 +1801,47 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
1672
1801
|
});
|
|
1673
1802
|
}, [iqAuthBaseUrl]);
|
|
1674
1803
|
const switchTo = async (tenantId) => {
|
|
1804
|
+
setSwitchError(null);
|
|
1805
|
+
setPendingStep(null);
|
|
1675
1806
|
try {
|
|
1676
|
-
await
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1807
|
+
const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
|
|
1808
|
+
if (result.kind === "mfa_required") {
|
|
1809
|
+
setPendingStep({
|
|
1810
|
+
kind: "mfa_required",
|
|
1811
|
+
tenantId: result.tenantId,
|
|
1812
|
+
mfaChallengeToken: result.mfaChallengeToken,
|
|
1813
|
+
availableMethods: result.availableMethods
|
|
1814
|
+
});
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (result.kind === "scope_selection_required") {
|
|
1818
|
+
setPendingStep({
|
|
1819
|
+
kind: "scope_selection_required",
|
|
1820
|
+
tenantId: result.tenantId,
|
|
1821
|
+
scopeSelectionToken: result.scopeSelectionToken
|
|
1822
|
+
});
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1681
1825
|
setActiveTenantId(tenantId);
|
|
1682
1826
|
setOpen(false);
|
|
1683
1827
|
onSwitched?.(tenantId);
|
|
1684
|
-
} catch {
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
setSwitchError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
const hostedSignInUrl = () => {
|
|
1833
|
+
const baseHosted = `${iqAuthBaseUrl.replace(/\/$/, "")}/sign-in`;
|
|
1834
|
+
if (!pendingStep) return baseHosted;
|
|
1835
|
+
const params = new URLSearchParams();
|
|
1836
|
+
if (pendingStep.kind === "mfa_required") {
|
|
1837
|
+
params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
|
|
1838
|
+
if (pendingStep.availableMethods.length) {
|
|
1839
|
+
params.set("availableMethods", pendingStep.availableMethods.join(","));
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
params.set("prompt", "login");
|
|
1685
1843
|
}
|
|
1844
|
+
return `${baseHosted}?${params.toString()}`;
|
|
1686
1845
|
};
|
|
1687
1846
|
const active = memberships.find((m) => m.tenantId === activeTenantId);
|
|
1688
1847
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1704,44 +1863,274 @@ function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearan
|
|
|
1704
1863
|
children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
|
|
1705
1864
|
}
|
|
1706
1865
|
),
|
|
1707
|
-
open ? /* @__PURE__ */
|
|
1866
|
+
open ? /* @__PURE__ */ jsxs("div", { role: "menu", style: {
|
|
1708
1867
|
position: "absolute",
|
|
1709
1868
|
left: 0,
|
|
1710
1869
|
top: 36,
|
|
1711
|
-
minWidth:
|
|
1870
|
+
minWidth: 260,
|
|
1712
1871
|
background: "#fff",
|
|
1713
1872
|
border: "1px solid rgba(15,23,42,0.12)",
|
|
1714
1873
|
borderRadius: 8,
|
|
1715
1874
|
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
1716
1875
|
padding: 8,
|
|
1717
1876
|
zIndex: 100
|
|
1718
|
-
}, children:
|
|
1877
|
+
}, children: [
|
|
1878
|
+
memberships.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: t2("orgSwitcher.noOrgs") }) : memberships.map((m) => /* @__PURE__ */ jsxs(
|
|
1879
|
+
"button",
|
|
1880
|
+
{
|
|
1881
|
+
role: "menuitem",
|
|
1882
|
+
type: "button",
|
|
1883
|
+
onClick: () => switchTo(m.tenantId),
|
|
1884
|
+
style: {
|
|
1885
|
+
display: "block",
|
|
1886
|
+
width: "100%",
|
|
1887
|
+
textAlign: "left",
|
|
1888
|
+
padding: "8px 10px",
|
|
1889
|
+
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
1890
|
+
border: "none",
|
|
1891
|
+
borderRadius: 4,
|
|
1892
|
+
cursor: "pointer",
|
|
1893
|
+
fontSize: 13,
|
|
1894
|
+
color: "#0f172a"
|
|
1895
|
+
},
|
|
1896
|
+
children: [
|
|
1897
|
+
/* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
1898
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
1899
|
+
]
|
|
1900
|
+
},
|
|
1901
|
+
m.tenantId
|
|
1902
|
+
)),
|
|
1903
|
+
pendingStep ? /* @__PURE__ */ jsxs(
|
|
1904
|
+
"div",
|
|
1905
|
+
{
|
|
1906
|
+
"data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-switch-mfa-required" : "prompt-org-switch-scope-required",
|
|
1907
|
+
role: "alert",
|
|
1908
|
+
style: {
|
|
1909
|
+
marginTop: 8,
|
|
1910
|
+
padding: "10px 12px",
|
|
1911
|
+
background: `${accent}10`,
|
|
1912
|
+
border: `1px solid ${accent}55`,
|
|
1913
|
+
borderRadius: 6
|
|
1914
|
+
},
|
|
1915
|
+
children: [
|
|
1916
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 12, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
|
|
1917
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
|
|
1918
|
+
/* @__PURE__ */ jsx(
|
|
1919
|
+
"a",
|
|
1920
|
+
{
|
|
1921
|
+
href: hostedSignInUrl(),
|
|
1922
|
+
"data-testid": "link-org-switch-continue-hosted",
|
|
1923
|
+
style: {
|
|
1924
|
+
display: "inline-block",
|
|
1925
|
+
fontSize: 12,
|
|
1926
|
+
fontWeight: 600,
|
|
1927
|
+
color: branding?.accentColor || accent,
|
|
1928
|
+
textDecoration: "none"
|
|
1929
|
+
},
|
|
1930
|
+
children: t2("orgSwitcher.continueInHostedSignIn")
|
|
1931
|
+
}
|
|
1932
|
+
)
|
|
1933
|
+
]
|
|
1934
|
+
}
|
|
1935
|
+
) : null,
|
|
1936
|
+
switchError ? /* @__PURE__ */ jsx("p", { "data-testid": "text-org-switch-error", style: { fontSize: 12, color: "#b91c1c", padding: "6px 8px 0" }, children: switchError }) : null
|
|
1937
|
+
] }) : null
|
|
1938
|
+
]
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
function useMemberships() {
|
|
1943
|
+
const { manager, snapshot } = useCtx();
|
|
1944
|
+
const [memberships, setMemberships] = useState([]);
|
|
1945
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1946
|
+
const [error, setError] = useState(null);
|
|
1947
|
+
const base = manager.issuerUrl.replace(/\/$/, "");
|
|
1948
|
+
const refresh = useCallback(async () => {
|
|
1949
|
+
setIsLoading(true);
|
|
1950
|
+
setError(null);
|
|
1951
|
+
try {
|
|
1952
|
+
const res = await manager.fetch(`${base}/api/v1/auth/available-scopes`);
|
|
1953
|
+
const json = await res.json().catch(() => ({}));
|
|
1954
|
+
if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
|
|
1955
|
+
const tree = json.data || {};
|
|
1956
|
+
const flat = [];
|
|
1957
|
+
for (const v of tree.vendors || []) {
|
|
1958
|
+
flat.push({
|
|
1959
|
+
membershipId: v.membershipId,
|
|
1960
|
+
scopeType: "vendor",
|
|
1961
|
+
scopeId: v.id,
|
|
1962
|
+
scopeName: v.name,
|
|
1963
|
+
role: v.role,
|
|
1964
|
+
grantedVia: v.grantedVia || "direct"
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
for (const s of tree.sources || []) {
|
|
1968
|
+
flat.push({
|
|
1969
|
+
membershipId: s.membershipId,
|
|
1970
|
+
scopeType: "source",
|
|
1971
|
+
scopeId: s.id,
|
|
1972
|
+
scopeName: s.name,
|
|
1973
|
+
role: s.role,
|
|
1974
|
+
grantedVia: s.grantedVia || "direct"
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
for (const c of tree.clients || []) {
|
|
1978
|
+
flat.push({
|
|
1979
|
+
membershipId: c.membershipId,
|
|
1980
|
+
scopeType: "client",
|
|
1981
|
+
scopeId: c.id,
|
|
1982
|
+
scopeName: c.name,
|
|
1983
|
+
role: c.role,
|
|
1984
|
+
grantedVia: c.grantedVia || "direct"
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
setMemberships(flat);
|
|
1988
|
+
} catch (err) {
|
|
1989
|
+
setError(err.message);
|
|
1990
|
+
setMemberships([]);
|
|
1991
|
+
} finally {
|
|
1992
|
+
setIsLoading(false);
|
|
1993
|
+
}
|
|
1994
|
+
}, [manager, base]);
|
|
1995
|
+
useEffect(() => {
|
|
1996
|
+
if (snapshot.status !== "authenticated") {
|
|
1997
|
+
setMemberships([]);
|
|
1998
|
+
setIsLoading(false);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
void refresh();
|
|
2002
|
+
}, [snapshot.status, snapshot.tenantId, refresh]);
|
|
2003
|
+
const switchScope = useCallback(
|
|
2004
|
+
(target) => performScopeSwitch(manager, base, target),
|
|
2005
|
+
[manager, base]
|
|
2006
|
+
);
|
|
2007
|
+
const active = useMemo(() => {
|
|
2008
|
+
const ctx = snapshot.user?.scopeContext;
|
|
2009
|
+
if (!ctx) return null;
|
|
2010
|
+
return {
|
|
2011
|
+
type: ctx.type,
|
|
2012
|
+
id: ctx.id,
|
|
2013
|
+
role: ctx.role,
|
|
2014
|
+
membershipId: ctx.membershipId
|
|
2015
|
+
};
|
|
2016
|
+
}, [snapshot.user, snapshot.version]);
|
|
2017
|
+
return { isLoading, error, memberships, active, refresh, switchScope };
|
|
2018
|
+
}
|
|
2019
|
+
function ScopeSwitcher({ onSwitched, include, className }) {
|
|
2020
|
+
const { memberships, active, isLoading, error, switchScope } = useMemberships();
|
|
2021
|
+
const [open, setOpen] = useState(false);
|
|
2022
|
+
const [busy, setBusy] = useState(null);
|
|
2023
|
+
const [switchError, setSwitchError] = useState(null);
|
|
2024
|
+
const visible = useMemo(
|
|
2025
|
+
() => include ? memberships.filter((m) => include.includes(m.scopeType)) : memberships,
|
|
2026
|
+
[memberships, include]
|
|
2027
|
+
);
|
|
2028
|
+
if (isLoading) {
|
|
2029
|
+
return createElement(
|
|
2030
|
+
"div",
|
|
2031
|
+
{ className, "data-testid": "scope-switcher-loading", style: { fontSize: 13, opacity: 0.6 } },
|
|
2032
|
+
"Loading scopes\u2026"
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
if (error) {
|
|
2036
|
+
return createElement(
|
|
2037
|
+
"div",
|
|
2038
|
+
{ className, "data-testid": "scope-switcher-error", style: { fontSize: 13, color: "#b91c1c" } },
|
|
2039
|
+
error
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
if (!visible.length) return null;
|
|
2043
|
+
const activeLabel = active ? visible.find((m) => m.scopeType === active.type && m.scopeId === active.id)?.scopeName || `${active.type}:${active.id}` : "Select a scope";
|
|
2044
|
+
return createElement(
|
|
2045
|
+
"div",
|
|
2046
|
+
{ className, "data-testid": "scope-switcher", style: { position: "relative", display: "inline-block" } },
|
|
2047
|
+
createElement(
|
|
2048
|
+
"button",
|
|
2049
|
+
{
|
|
2050
|
+
type: "button",
|
|
2051
|
+
"data-testid": "scope-switcher-trigger",
|
|
2052
|
+
onClick: () => setOpen((v) => !v),
|
|
2053
|
+
style: {
|
|
2054
|
+
padding: "6px 10px",
|
|
2055
|
+
border: "1px solid #e5e7eb",
|
|
2056
|
+
borderRadius: 6,
|
|
2057
|
+
background: "#fff",
|
|
2058
|
+
cursor: "pointer",
|
|
2059
|
+
fontSize: 13
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
active ? `${active.type}: ${activeLabel}` : activeLabel
|
|
2063
|
+
),
|
|
2064
|
+
switchError ? createElement(
|
|
2065
|
+
"div",
|
|
2066
|
+
{
|
|
2067
|
+
"data-testid": "scope-switcher-switch-error",
|
|
2068
|
+
style: { marginTop: 4, fontSize: 12, color: "#b91c1c" }
|
|
2069
|
+
},
|
|
2070
|
+
switchError
|
|
2071
|
+
) : null,
|
|
2072
|
+
open ? createElement(
|
|
2073
|
+
"div",
|
|
2074
|
+
{
|
|
2075
|
+
"data-testid": "scope-switcher-menu",
|
|
2076
|
+
style: {
|
|
2077
|
+
position: "absolute",
|
|
2078
|
+
top: "100%",
|
|
2079
|
+
left: 0,
|
|
2080
|
+
marginTop: 4,
|
|
2081
|
+
minWidth: 220,
|
|
2082
|
+
background: "#fff",
|
|
2083
|
+
border: "1px solid #e5e7eb",
|
|
2084
|
+
borderRadius: 6,
|
|
2085
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
2086
|
+
padding: 4,
|
|
2087
|
+
zIndex: 50
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
2090
|
+
visible.map((m) => {
|
|
2091
|
+
const isActive = active?.type === m.scopeType && active?.id === m.scopeId;
|
|
2092
|
+
const key = `${m.scopeType}:${m.scopeId}`;
|
|
2093
|
+
return createElement(
|
|
1719
2094
|
"button",
|
|
1720
2095
|
{
|
|
1721
|
-
|
|
2096
|
+
key,
|
|
1722
2097
|
type: "button",
|
|
1723
|
-
|
|
2098
|
+
"data-testid": `scope-switcher-option-${m.scopeType}-${m.scopeId}`,
|
|
2099
|
+
disabled: busy === key,
|
|
2100
|
+
onClick: async () => {
|
|
2101
|
+
setBusy(key);
|
|
2102
|
+
setSwitchError(null);
|
|
2103
|
+
try {
|
|
2104
|
+
await switchScope({ type: m.scopeType, id: m.scopeId });
|
|
2105
|
+
setOpen(false);
|
|
2106
|
+
onSwitched?.({ type: m.scopeType, id: m.scopeId });
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
setSwitchError(err.message || "Scope switch failed");
|
|
2109
|
+
} finally {
|
|
2110
|
+
setBusy(null);
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
1724
2113
|
style: {
|
|
1725
2114
|
display: "block",
|
|
1726
2115
|
width: "100%",
|
|
1727
2116
|
textAlign: "left",
|
|
1728
2117
|
padding: "8px 10px",
|
|
1729
|
-
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
1730
2118
|
border: "none",
|
|
2119
|
+
background: isActive ? "#f3f4f6" : "transparent",
|
|
2120
|
+
cursor: busy ? "wait" : "pointer",
|
|
1731
2121
|
borderRadius: 4,
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
color: "#0f172a"
|
|
1735
|
-
},
|
|
1736
|
-
children: [
|
|
1737
|
-
/* @__PURE__ */ jsx("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
1738
|
-
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
1739
|
-
]
|
|
2122
|
+
fontSize: 13
|
|
2123
|
+
}
|
|
1740
2124
|
},
|
|
1741
|
-
m.
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2125
|
+
createElement("div", { style: { fontWeight: 500 } }, m.scopeName),
|
|
2126
|
+
createElement(
|
|
2127
|
+
"div",
|
|
2128
|
+
{ style: { fontSize: 11, opacity: 0.6 } },
|
|
2129
|
+
`${m.scopeType} \xB7 ${m.role}${m.grantedVia && m.grantedVia !== "direct" ? ` \xB7 via ${m.grantedVia}` : ""}`
|
|
2130
|
+
)
|
|
2131
|
+
);
|
|
2132
|
+
})
|
|
2133
|
+
) : null
|
|
1745
2134
|
);
|
|
1746
2135
|
}
|
|
1747
2136
|
function useImpersonation() {
|
|
@@ -2354,10 +2743,13 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
2354
2743
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
2355
2744
|
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
2356
2745
|
const accent = branding?.accentColor || "#6366f1";
|
|
2746
|
+
const t2 = useT();
|
|
2747
|
+
const { manager } = useCtx();
|
|
2357
2748
|
const [memberships, setMemberships] = useState([]);
|
|
2358
2749
|
const [activeTenantId, setActiveTenantId] = useState(null);
|
|
2359
2750
|
const [loading, setLoading] = useState(true);
|
|
2360
2751
|
const [error, setError] = useState(null);
|
|
2752
|
+
const [pendingStep, setPendingStep] = useState(null);
|
|
2361
2753
|
useEffect(() => {
|
|
2362
2754
|
let cancelled = false;
|
|
2363
2755
|
Promise.all([
|
|
@@ -2381,21 +2773,81 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
2381
2773
|
onSelect?.(tenantId);
|
|
2382
2774
|
return;
|
|
2383
2775
|
}
|
|
2776
|
+
setError(null);
|
|
2777
|
+
setPendingStep(null);
|
|
2384
2778
|
try {
|
|
2385
|
-
await
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2779
|
+
const result = await performTenantSwitch(manager, iqAuthBaseUrl, tenantId);
|
|
2780
|
+
if (result.kind === "mfa_required") {
|
|
2781
|
+
setPendingStep({
|
|
2782
|
+
kind: "mfa_required",
|
|
2783
|
+
tenantId: result.tenantId,
|
|
2784
|
+
mfaChallengeToken: result.mfaChallengeToken,
|
|
2785
|
+
availableMethods: result.availableMethods
|
|
2786
|
+
});
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (result.kind === "scope_selection_required") {
|
|
2790
|
+
setPendingStep({
|
|
2791
|
+
kind: "scope_selection_required",
|
|
2792
|
+
tenantId: result.tenantId,
|
|
2793
|
+
scopeSelectionToken: result.scopeSelectionToken
|
|
2794
|
+
});
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2391
2797
|
setActiveTenantId(tenantId);
|
|
2392
2798
|
onSelect?.(tenantId);
|
|
2393
2799
|
} catch (err) {
|
|
2394
2800
|
setError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
2395
2801
|
}
|
|
2396
2802
|
};
|
|
2803
|
+
const hostedSignInUrl = () => {
|
|
2804
|
+
const baseHosted = `${baseUrl}/sign-in`;
|
|
2805
|
+
if (!pendingStep) return baseHosted;
|
|
2806
|
+
const params = new URLSearchParams();
|
|
2807
|
+
if (pendingStep.kind === "mfa_required") {
|
|
2808
|
+
params.set("mfaChallengeToken", pendingStep.mfaChallengeToken);
|
|
2809
|
+
if (pendingStep.availableMethods.length) {
|
|
2810
|
+
params.set("availableMethods", pendingStep.availableMethods.join(","));
|
|
2811
|
+
}
|
|
2812
|
+
} else {
|
|
2813
|
+
params.set("prompt", "login");
|
|
2814
|
+
}
|
|
2815
|
+
return `${baseHosted}?${params.toString()}`;
|
|
2816
|
+
};
|
|
2397
2817
|
return /* @__PURE__ */ jsx(Shell, { appearance, branding, className, title: "Your organizations", subtitle: "Select an organization to make it active.", children: /* @__PURE__ */ jsxs("div", { "data-iqauth-sdk-org-list": "", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
2398
2818
|
error ? /* @__PURE__ */ jsx(ErrorBanner, { message: error }) : null,
|
|
2819
|
+
pendingStep ? /* @__PURE__ */ jsxs(
|
|
2820
|
+
"div",
|
|
2821
|
+
{
|
|
2822
|
+
"data-testid": pendingStep.kind === "mfa_required" ? "prompt-org-list-mfa-required" : "prompt-org-list-scope-required",
|
|
2823
|
+
role: "alert",
|
|
2824
|
+
style: {
|
|
2825
|
+
padding: "10px 12px",
|
|
2826
|
+
background: `${accent}10`,
|
|
2827
|
+
border: `1px solid ${accent}55`,
|
|
2828
|
+
borderRadius: 6
|
|
2829
|
+
},
|
|
2830
|
+
children: [
|
|
2831
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a", marginBottom: 4 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredTitle") : t2("orgSwitcher.scopeSelectionRequiredTitle") }),
|
|
2832
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#334155", marginBottom: 8 }, children: pendingStep.kind === "mfa_required" ? t2("orgSwitcher.mfaRequiredBody") : t2("orgSwitcher.scopeSelectionRequiredBody") }),
|
|
2833
|
+
/* @__PURE__ */ jsx(
|
|
2834
|
+
"a",
|
|
2835
|
+
{
|
|
2836
|
+
href: hostedSignInUrl(),
|
|
2837
|
+
"data-testid": "link-org-list-continue-hosted",
|
|
2838
|
+
style: {
|
|
2839
|
+
display: "inline-block",
|
|
2840
|
+
fontSize: 12,
|
|
2841
|
+
fontWeight: 600,
|
|
2842
|
+
color: branding?.accentColor || accent,
|
|
2843
|
+
textDecoration: "none"
|
|
2844
|
+
},
|
|
2845
|
+
children: t2("orgSwitcher.continueInHostedSignIn")
|
|
2846
|
+
}
|
|
2847
|
+
)
|
|
2848
|
+
]
|
|
2849
|
+
}
|
|
2850
|
+
) : null,
|
|
2399
2851
|
loading ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13 }, children: "Loading\u2026" }) : memberships.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: "You don\u2019t belong to any organizations yet." }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: memberships.map((m) => {
|
|
2400
2852
|
const active = m.tenantId === activeTenantId;
|
|
2401
2853
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
@@ -2433,8 +2885,8 @@ function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRe
|
|
|
2433
2885
|
iqAuthBaseUrl,
|
|
2434
2886
|
unstyled: true,
|
|
2435
2887
|
appearance,
|
|
2436
|
-
onCreated: (
|
|
2437
|
-
onSelect?.(
|
|
2888
|
+
onCreated: (t3) => {
|
|
2889
|
+
onSelect?.(t3.id);
|
|
2438
2890
|
reloadList();
|
|
2439
2891
|
},
|
|
2440
2892
|
redirectUrl: createRedirectUrl
|
|
@@ -2670,8 +3122,6 @@ function LinkedAccounts({ className, onChange, ...rest }) {
|
|
|
2670
3122
|
var __version__ = "phase-bc-1.0.0";
|
|
2671
3123
|
|
|
2672
3124
|
export {
|
|
2673
|
-
sanitizeReturnTo,
|
|
2674
|
-
isReturnToAllowed,
|
|
2675
3125
|
IQAuthProvider,
|
|
2676
3126
|
__useIQAuthInternal,
|
|
2677
3127
|
useLocale,
|
|
@@ -2691,6 +3141,9 @@ export {
|
|
|
2691
3141
|
IQAuthLoading,
|
|
2692
3142
|
IQAuthLoaded,
|
|
2693
3143
|
RedirectToSignIn,
|
|
3144
|
+
performScopeSwitch,
|
|
3145
|
+
performTenantSwitch,
|
|
3146
|
+
claimSatisfiesScope,
|
|
2694
3147
|
Protect,
|
|
2695
3148
|
RedirectToSignedIn,
|
|
2696
3149
|
useReturnTo,
|
|
@@ -2701,11 +3154,14 @@ export {
|
|
|
2701
3154
|
sanitizeBrandCss,
|
|
2702
3155
|
useResolvedSdkBranding,
|
|
2703
3156
|
isSilentSsoEligible,
|
|
3157
|
+
resolveAfterSignInDestination,
|
|
2704
3158
|
SignIn,
|
|
2705
3159
|
SignUp,
|
|
2706
3160
|
UserButton,
|
|
2707
3161
|
UserProfile,
|
|
2708
3162
|
OrganizationSwitcher,
|
|
3163
|
+
useMemberships,
|
|
3164
|
+
ScopeSwitcher,
|
|
2709
3165
|
useImpersonation,
|
|
2710
3166
|
ImpersonationBanner,
|
|
2711
3167
|
useReverification,
|