@iqauth/sdk 2.3.0 → 2.6.0
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/README.md +110 -0
- package/dist/browser-session.d.mts +3 -2
- package/dist/browser-session.d.ts +3 -2
- package/dist/browser.d.mts +64 -29
- package/dist/browser.d.ts +64 -29
- package/dist/browser.js +782 -38
- package/dist/browser.mjs +43 -3
- package/dist/bundle-LUKDQYVQ.mjs +374 -0
- package/dist/chunk-3JULWS6F.mjs +106 -0
- package/dist/chunk-5T7GHBX6.mjs +1165 -0
- package/dist/{chunk-KGEPDXHU.mjs → chunk-6TDJJER7.mjs} +2 -2
- package/dist/{chunk-RACIPVLD.mjs → chunk-76W5TLQQ.mjs} +262 -220
- package/dist/{chunk-EKTNEZIH.mjs → chunk-BVV54LPI.mjs} +37 -5
- package/dist/chunk-LIZYFXH7.mjs +90 -0
- package/dist/chunk-MKKZULZR.mjs +241 -0
- package/dist/chunk-SL3KRS4W.mjs +54 -0
- package/dist/chunk-TKZTCPEK.mjs +232 -0
- package/dist/chunk-UKZLOHZG.mjs +83 -0
- package/dist/cli/index.js +144 -36
- package/dist/cli/index.mjs +1 -1
- package/dist/{client-DTX4hNdS.d.ts → client-BNQe3AgF.d.ts} +3 -62
- package/dist/{client-vdh2a9fJ.d.mts → client-kYlJFgPv.d.mts} +3 -62
- package/dist/doctor-YYNHNMLD.mjs +198 -0
- package/dist/{express-A0-dWEMy.d.mts → express-B6_1vBYZ.d.mts} +23 -2
- package/dist/{express-Bo_pJKHN.d.ts → express-CHpfa7D_.d.ts} +23 -2
- package/dist/express.d.mts +5 -4
- package/dist/express.d.ts +5 -4
- package/dist/express.js +36 -4
- package/dist/express.mjs +8 -8
- package/dist/fastify.js +2 -2
- package/dist/fastify.mjs +4 -4
- package/dist/hono.js +2 -2
- package/dist/hono.mjs +4 -4
- package/dist/index.d.mts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +500 -4
- package/dist/index.mjs +29 -9
- package/dist/locales.d.mts +53 -0
- package/dist/locales.d.ts +53 -0
- package/dist/locales.js +1202 -0
- package/dist/locales.mjs +29 -0
- package/dist/mobile.d.mts +3 -2
- package/dist/mobile.d.ts +3 -2
- package/dist/next.d.mts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +2 -2
- package/dist/next.mjs +1 -1
- package/dist/provisioningBridge-88xjOS2n.d.mts +86 -0
- package/dist/provisioningBridge-DnTfzdZK.d.ts +86 -0
- package/dist/react.d.mts +1349 -10
- package/dist/react.d.ts +1349 -10
- package/dist/react.js +3006 -568
- package/dist/react.mjs +1540 -97
- package/dist/reverify-4UEJXUS6.mjs +16 -0
- package/dist/server/handlers.d.mts +10 -1
- package/dist/server/handlers.d.ts +10 -1
- package/dist/server/handlers.js +2 -2
- package/dist/server/handlers.mjs +1 -1
- package/dist/server.d.mts +5 -3
- package/dist/server.d.ts +5 -3
- package/dist/server.js +89 -4
- package/dist/server.mjs +12 -8
- package/dist/service.d.mts +3 -2
- package/dist/service.d.ts +3 -2
- package/dist/signIn-CCY4JE5G.mjs +15 -0
- package/dist/{signIn-Cd0P4y9d.d.mts → signIn-CiIBTJIh.d.mts} +224 -4
- package/dist/{signIn-DKakyzeu.d.ts → signIn-OCr88Zf8.d.ts} +224 -4
- package/dist/test.d.mts +86 -0
- package/dist/test.d.ts +86 -0
- package/dist/test.js +289 -0
- package/dist/test.mjs +9 -0
- package/dist/tokens-DCyzzn8L.d.mts +63 -0
- package/dist/tokens-aHiGFr_E.d.ts +63 -0
- package/dist/types-6bNdxesb.d.mts +196 -0
- package/dist/types-6bNdxesb.d.ts +196 -0
- package/dist/{types-Cxl3bQHt.d.mts → types-DZAflmmq.d.mts} +6 -0
- package/dist/{types-Cxl3bQHt.d.ts → types-DZAflmmq.d.ts} +6 -0
- package/dist/webhooks.d.mts +61 -0
- package/dist/webhooks.d.ts +61 -0
- package/dist/webhooks.js +119 -0
- package/dist/webhooks.mjs +11 -0
- package/dist/ws.d.mts +73 -0
- package/dist/ws.d.ts +73 -0
- package/dist/ws.js +397 -0
- package/dist/ws.mjs +12 -0
- package/package.json +22 -2
- package/dist/doctor-A5E7LSFW.mjs +0 -90
package/dist/react.js
CHANGED
|
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
6
9
|
var __export = (target, all) => {
|
|
7
10
|
for (var name in all)
|
|
8
11
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -17,120 +20,24 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
20
|
};
|
|
18
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
22
|
|
|
20
|
-
// src/react.ts
|
|
21
|
-
var react_exports = {};
|
|
22
|
-
__export(react_exports, {
|
|
23
|
-
AuthCallback: () => AuthCallback,
|
|
24
|
-
IQAuthLoaded: () => IQAuthLoaded,
|
|
25
|
-
IQAuthLoading: () => IQAuthLoading,
|
|
26
|
-
IQAuthProvider: () => IQAuthProvider,
|
|
27
|
-
OrganizationSwitcher: () => OrganizationSwitcher,
|
|
28
|
-
RedirectToSignIn: () => RedirectToSignIn,
|
|
29
|
-
SignIn: () => SignIn,
|
|
30
|
-
SignUp: () => SignUp,
|
|
31
|
-
SignedIn: () => SignedIn,
|
|
32
|
-
SignedOut: () => SignedOut,
|
|
33
|
-
UserButton: () => UserButton,
|
|
34
|
-
UserProfile: () => UserProfile,
|
|
35
|
-
__version__: () => __version__,
|
|
36
|
-
isSilentSsoEligible: () => isSilentSsoEligible,
|
|
37
|
-
sanitizeBrandCss: () => sanitizeBrandCss,
|
|
38
|
-
useAuth: () => useAuth,
|
|
39
|
-
useAuthFetch: () => useAuthFetch,
|
|
40
|
-
useIQAuthSignInContext: () => useIQAuthSignInContext,
|
|
41
|
-
useOrganization: () => useOrganization,
|
|
42
|
-
useResolvedSdkBranding: () => useResolvedSdkBranding,
|
|
43
|
-
useSession: () => useSession,
|
|
44
|
-
useUser: () => useUser
|
|
45
|
-
});
|
|
46
|
-
module.exports = __toCommonJS(react_exports);
|
|
47
|
-
|
|
48
|
-
// src/react/index.tsx
|
|
49
|
-
var import_react = require("react");
|
|
50
|
-
|
|
51
23
|
// src/errors.ts
|
|
52
|
-
var IQAuthError
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
66
|
-
if (typeof atob === "function") {
|
|
67
|
-
const bin = atob(normalized);
|
|
68
|
-
const bytes = new Uint8Array(bin.length);
|
|
69
|
-
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
70
|
-
return new TextDecoder().decode(bytes);
|
|
71
|
-
}
|
|
72
|
-
const { Buffer: Buffer2 } = require("buffer");
|
|
73
|
-
return Buffer2.from(normalized, "base64").toString("utf8");
|
|
74
|
-
}
|
|
75
|
-
function isValidIssuerUrl(iss) {
|
|
76
|
-
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
77
|
-
if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
|
|
78
|
-
try {
|
|
79
|
-
const u = new URL(iss);
|
|
80
|
-
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
81
|
-
if (!u.hostname) return false;
|
|
82
|
-
return true;
|
|
83
|
-
} catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function assertPublishableKey(raw, opts) {
|
|
88
|
-
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
89
|
-
if (typeof raw !== "string" || raw.length === 0) {
|
|
90
|
-
throw new IQAuthError(
|
|
91
|
-
"CONFIG_INVALID",
|
|
92
|
-
`${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
96
|
-
if (!shapeMatch) {
|
|
97
|
-
throw new IQAuthError(
|
|
98
|
-
"CONFIG_INVALID",
|
|
99
|
-
`${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
let decoded;
|
|
103
|
-
try {
|
|
104
|
-
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
105
|
-
} catch {
|
|
106
|
-
throw new IQAuthError(
|
|
107
|
-
"CONFIG_INVALID",
|
|
108
|
-
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
if (!isPublishableKeyPayload(decoded)) {
|
|
112
|
-
throw new IQAuthError(
|
|
113
|
-
"CONFIG_INVALID",
|
|
114
|
-
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (!isValidIssuerUrl(decoded.iss)) {
|
|
118
|
-
throw new IQAuthError(
|
|
119
|
-
"CONFIG_INVALID",
|
|
120
|
-
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
|
|
121
|
-
);
|
|
24
|
+
var IQAuthError;
|
|
25
|
+
var init_errors = __esm({
|
|
26
|
+
"src/errors.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
IQAuthError = class extends Error {
|
|
29
|
+
constructor(code, message, status, raw) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "IQAuthError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.raw = raw;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
122
37
|
}
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
function isPublishableKeyPayload(value) {
|
|
126
|
-
if (!value || typeof value !== "object") return false;
|
|
127
|
-
const v = value;
|
|
128
|
-
return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
|
|
129
|
-
}
|
|
38
|
+
});
|
|
130
39
|
|
|
131
40
|
// src/browser/storage.ts
|
|
132
|
-
var REFRESH_COOKIE = "iqauth_rt";
|
|
133
|
-
var PKCE_STORAGE_PREFIX = "iqauth.pkce.";
|
|
134
41
|
function isBrowser() {
|
|
135
42
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
136
43
|
}
|
|
@@ -187,105 +94,918 @@ function clearPkce(state) {
|
|
|
187
94
|
} catch {
|
|
188
95
|
}
|
|
189
96
|
}
|
|
97
|
+
var REFRESH_COOKIE, PKCE_STORAGE_PREFIX;
|
|
98
|
+
var init_storage = __esm({
|
|
99
|
+
"src/browser/storage.ts"() {
|
|
100
|
+
"use strict";
|
|
101
|
+
REFRESH_COOKIE = "iqauth_rt";
|
|
102
|
+
PKCE_STORAGE_PREFIX = "iqauth.pkce.";
|
|
103
|
+
}
|
|
104
|
+
});
|
|
190
105
|
|
|
191
|
-
// src/browser/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
const parts = token.split(".");
|
|
197
|
-
if (parts.length !== 3) return null;
|
|
198
|
-
const json = typeof atob === "function" ? decodeURIComponent(
|
|
199
|
-
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
200
|
-
) : Buffer.from(parts[1], "base64url").toString("utf8");
|
|
201
|
-
return JSON.parse(json);
|
|
202
|
-
} catch {
|
|
203
|
-
return null;
|
|
106
|
+
// src/browser/pkce.ts
|
|
107
|
+
function getCrypto() {
|
|
108
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
109
|
+
return globalThis.crypto;
|
|
204
110
|
}
|
|
111
|
+
throw new Error("WebCrypto is not available in this environment");
|
|
205
112
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
name: claims.name,
|
|
212
|
-
tenantId: claims.tenantId,
|
|
213
|
-
vendorId: claims.vendorId,
|
|
214
|
-
roles: claims.roles ?? [],
|
|
215
|
-
entitlements: claims.entitlements ?? []
|
|
216
|
-
};
|
|
113
|
+
function base64UrlEncode(bytes) {
|
|
114
|
+
let bin = "";
|
|
115
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
116
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
|
|
117
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
217
118
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
claims: null,
|
|
223
|
-
tenantId: null,
|
|
224
|
-
error: null,
|
|
225
|
-
version: 0
|
|
226
|
-
};
|
|
227
|
-
function defaultCookieStore() {
|
|
228
|
-
return {
|
|
229
|
-
read: () => getCookie(REFRESH_COOKIE),
|
|
230
|
-
write: (token) => setCookie(REFRESH_COOKIE, token, { maxAgeSeconds: 60 * 60 * 24 * 30 }),
|
|
231
|
-
clear: () => clearCookie(REFRESH_COOKIE)
|
|
232
|
-
};
|
|
119
|
+
function randomUrlSafe(byteLength = 32) {
|
|
120
|
+
const bytes = new Uint8Array(byteLength);
|
|
121
|
+
getCrypto().getRandomValues(bytes);
|
|
122
|
+
return base64UrlEncode(bytes);
|
|
233
123
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
/** Active claims by other tabs (keyed by source tabId). */
|
|
250
|
-
this.foreignClaim = null;
|
|
251
|
-
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
|
|
252
|
-
this.key = parsed;
|
|
253
|
-
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
254
|
-
this.issuer = inferred.replace(/\/+$/, "");
|
|
255
|
-
this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
256
|
-
this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
|
|
257
|
-
this.useCookies = options.useCookies ?? true;
|
|
258
|
-
this.serverManagedSession = options.serverManagedSession ?? false;
|
|
259
|
-
this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
|
|
260
|
-
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore() : NO_OP_STORE);
|
|
261
|
-
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
262
|
-
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
263
|
-
throw new Error("global fetch is not available; pass fetchImpl");
|
|
264
|
-
}));
|
|
265
|
-
this.tabId = Math.random().toString(36).slice(2);
|
|
266
|
-
if (typeof BroadcastChannel !== "undefined") {
|
|
267
|
-
const name = options.channelName ?? `iqauth.${parsed.appId}`;
|
|
268
|
-
try {
|
|
269
|
-
this.channel = new BroadcastChannel(name);
|
|
270
|
-
this.channel.onmessage = (ev) => this.onBroadcast(ev.data);
|
|
271
|
-
} catch {
|
|
272
|
-
this.channel = null;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
124
|
+
async function s256Challenge(verifier) {
|
|
125
|
+
const data = new TextEncoder().encode(verifier);
|
|
126
|
+
const digest = await getCrypto().subtle.digest("SHA-256", data);
|
|
127
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
128
|
+
}
|
|
129
|
+
async function createPkcePair() {
|
|
130
|
+
const codeVerifier = randomUrlSafe(32);
|
|
131
|
+
const codeChallenge = await s256Challenge(codeVerifier);
|
|
132
|
+
const state = randomUrlSafe(16);
|
|
133
|
+
const nonce = randomUrlSafe(16);
|
|
134
|
+
return { codeVerifier, codeChallenge, state, nonce };
|
|
135
|
+
}
|
|
136
|
+
var init_pkce = __esm({
|
|
137
|
+
"src/browser/pkce.ts"() {
|
|
138
|
+
"use strict";
|
|
275
139
|
}
|
|
276
|
-
|
|
277
|
-
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// src/browser/signIn.ts
|
|
143
|
+
var signIn_exports = {};
|
|
144
|
+
__export(signIn_exports, {
|
|
145
|
+
buildSignInUrl: () => buildSignInUrl,
|
|
146
|
+
handleAuthCallback: () => handleAuthCallback,
|
|
147
|
+
redirectToSignIn: () => redirectToSignIn,
|
|
148
|
+
signIn: () => signIn,
|
|
149
|
+
signOut: () => signOut
|
|
150
|
+
});
|
|
151
|
+
function defaultRedirectUri() {
|
|
152
|
+
if (typeof window === "undefined") {
|
|
153
|
+
throw new Error("redirectToSignIn requires a browser environment (window)");
|
|
278
154
|
}
|
|
279
|
-
|
|
280
|
-
|
|
155
|
+
return `${window.location.origin}${DEFAULT_CALLBACK_PATH}`;
|
|
156
|
+
}
|
|
157
|
+
function defaultReturnTo() {
|
|
158
|
+
if (typeof window === "undefined") return "/";
|
|
159
|
+
return window.location.href;
|
|
160
|
+
}
|
|
161
|
+
async function buildSignInUrl(manager, opts = {}) {
|
|
162
|
+
const pkce = await createPkcePair();
|
|
163
|
+
const redirectUri = opts.redirectUri ?? defaultRedirectUri();
|
|
164
|
+
const returnTo = opts.returnTo ?? defaultReturnTo();
|
|
165
|
+
savePkce({
|
|
166
|
+
codeVerifier: pkce.codeVerifier,
|
|
167
|
+
state: pkce.state,
|
|
168
|
+
nonce: pkce.nonce,
|
|
169
|
+
redirectUri,
|
|
170
|
+
appKey: manager.publishableKey.raw,
|
|
171
|
+
returnTo,
|
|
172
|
+
createdAt: Date.now()
|
|
173
|
+
});
|
|
174
|
+
const url2 = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
|
|
175
|
+
url2.searchParams.set("response_type", "code");
|
|
176
|
+
url2.searchParams.set("app", manager.appKey);
|
|
177
|
+
url2.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
178
|
+
url2.searchParams.set("redirect_uri", redirectUri);
|
|
179
|
+
url2.searchParams.set("state", pkce.state);
|
|
180
|
+
url2.searchParams.set("nonce", pkce.nonce);
|
|
181
|
+
url2.searchParams.set("code_challenge", pkce.codeChallenge);
|
|
182
|
+
url2.searchParams.set("code_challenge_method", "S256");
|
|
183
|
+
url2.searchParams.set("scope", opts.scope ?? "openid profile email");
|
|
184
|
+
url2.searchParams.set("return_to", returnTo);
|
|
185
|
+
if (opts.prompt) url2.searchParams.set("prompt", opts.prompt);
|
|
186
|
+
return url2.toString();
|
|
187
|
+
}
|
|
188
|
+
async function redirectToSignIn(manager, opts = {}) {
|
|
189
|
+
const url2 = await buildSignInUrl(manager, opts);
|
|
190
|
+
if (typeof window === "undefined") {
|
|
191
|
+
throw new Error("redirectToSignIn requires a browser environment");
|
|
281
192
|
}
|
|
282
|
-
|
|
283
|
-
|
|
193
|
+
window.location.assign(url2);
|
|
194
|
+
}
|
|
195
|
+
async function signIn(manager, opts = {}) {
|
|
196
|
+
return redirectToSignIn(manager, opts);
|
|
197
|
+
}
|
|
198
|
+
async function handleAuthCallback(manager, options = {}) {
|
|
199
|
+
const url2 = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
200
|
+
const code = url2.searchParams.get("code");
|
|
201
|
+
const state = url2.searchParams.get("state");
|
|
202
|
+
const errorParam = url2.searchParams.get("error");
|
|
203
|
+
if (errorParam) {
|
|
204
|
+
return { ok: false, returnTo: "/", error: errorParam };
|
|
284
205
|
}
|
|
285
|
-
|
|
286
|
-
return
|
|
206
|
+
if (!code || !state) {
|
|
207
|
+
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
287
208
|
}
|
|
288
|
-
|
|
209
|
+
const record = loadPkce(state);
|
|
210
|
+
if (!record) {
|
|
211
|
+
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
212
|
+
}
|
|
213
|
+
clearPkce(state);
|
|
214
|
+
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
215
|
+
if (!fetchImpl) {
|
|
216
|
+
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
217
|
+
}
|
|
218
|
+
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
219
|
+
const res = await fetchImpl(tokenUrl, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
credentials: "include",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
grant_type: "authorization_code",
|
|
225
|
+
code,
|
|
226
|
+
redirect_uri: record.redirectUri,
|
|
227
|
+
client_id: manager.appKey,
|
|
228
|
+
code_verifier: record.codeVerifier
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
const body = await res.json().catch(() => ({}));
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
234
|
+
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
235
|
+
}
|
|
236
|
+
const tokens = body;
|
|
237
|
+
if (!tokens.access_token) {
|
|
238
|
+
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
239
|
+
}
|
|
240
|
+
if (tokens.refresh_token) {
|
|
241
|
+
const cookieName = options.cookieNames?.refresh ?? manager.refreshCookie ?? REFRESH_COOKIE;
|
|
242
|
+
setCookie(cookieName, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
243
|
+
}
|
|
244
|
+
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
245
|
+
return { ok: true, returnTo: record.returnTo };
|
|
246
|
+
}
|
|
247
|
+
async function signOut(manager, opts = {}) {
|
|
248
|
+
if (!opts.localOnly) {
|
|
249
|
+
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
250
|
+
try {
|
|
251
|
+
const url2 = `${issuer}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
252
|
+
await manager.fetch(url2, { method: "POST" }).catch(() => void 0);
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
if (opts.endSsoSession !== false) {
|
|
256
|
+
try {
|
|
257
|
+
await fetch(`${issuer}${DEFAULT_SSO_LOGOUT_PATH}`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
credentials: "include"
|
|
260
|
+
}).catch(() => void 0);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
clearCookie(manager.refreshCookie ?? REFRESH_COOKIE);
|
|
266
|
+
manager.signOutLocal();
|
|
267
|
+
if (opts.returnTo && typeof window !== "undefined") {
|
|
268
|
+
window.location.assign(opts.returnTo);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
var DEFAULT_SIGN_IN_PATH, DEFAULT_LOGOUT_PATH, DEFAULT_SSO_LOGOUT_PATH, DEFAULT_TOKEN_PATH, DEFAULT_CALLBACK_PATH;
|
|
272
|
+
var init_signIn = __esm({
|
|
273
|
+
"src/browser/signIn.ts"() {
|
|
274
|
+
"use strict";
|
|
275
|
+
init_pkce();
|
|
276
|
+
init_storage();
|
|
277
|
+
DEFAULT_SIGN_IN_PATH = "/sign-in";
|
|
278
|
+
DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
|
|
279
|
+
DEFAULT_SSO_LOGOUT_PATH = "/oidc/sso-logout";
|
|
280
|
+
DEFAULT_TOKEN_PATH = "/oidc/token";
|
|
281
|
+
DEFAULT_CALLBACK_PATH = "/auth/callback";
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ../../node_modules/@simplewebauthn/browser/dist/bundle/index.js
|
|
286
|
+
var bundle_exports = {};
|
|
287
|
+
__export(bundle_exports, {
|
|
288
|
+
WebAuthnAbortService: () => WebAuthnAbortService,
|
|
289
|
+
WebAuthnError: () => WebAuthnError,
|
|
290
|
+
base64URLStringToBuffer: () => base64URLStringToBuffer,
|
|
291
|
+
browserSupportsWebAuthn: () => browserSupportsWebAuthn,
|
|
292
|
+
browserSupportsWebAuthnAutofill: () => browserSupportsWebAuthnAutofill,
|
|
293
|
+
bufferToBase64URLString: () => bufferToBase64URLString,
|
|
294
|
+
platformAuthenticatorIsAvailable: () => platformAuthenticatorIsAvailable,
|
|
295
|
+
startAuthentication: () => startAuthentication,
|
|
296
|
+
startRegistration: () => startRegistration
|
|
297
|
+
});
|
|
298
|
+
function bufferToBase64URLString(buffer) {
|
|
299
|
+
const bytes = new Uint8Array(buffer);
|
|
300
|
+
let str = "";
|
|
301
|
+
for (const charCode of bytes) {
|
|
302
|
+
str += String.fromCharCode(charCode);
|
|
303
|
+
}
|
|
304
|
+
const base64String = btoa(str);
|
|
305
|
+
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
306
|
+
}
|
|
307
|
+
function base64URLStringToBuffer(base64URLString) {
|
|
308
|
+
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
|
|
309
|
+
const padLength = (4 - base64.length % 4) % 4;
|
|
310
|
+
const padded = base64.padEnd(base64.length + padLength, "=");
|
|
311
|
+
const binary = atob(padded);
|
|
312
|
+
const buffer = new ArrayBuffer(binary.length);
|
|
313
|
+
const bytes = new Uint8Array(buffer);
|
|
314
|
+
for (let i = 0; i < binary.length; i++) {
|
|
315
|
+
bytes[i] = binary.charCodeAt(i);
|
|
316
|
+
}
|
|
317
|
+
return buffer;
|
|
318
|
+
}
|
|
319
|
+
function browserSupportsWebAuthn() {
|
|
320
|
+
return window?.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
|
|
321
|
+
}
|
|
322
|
+
function toPublicKeyCredentialDescriptor(descriptor) {
|
|
323
|
+
const { id } = descriptor;
|
|
324
|
+
return {
|
|
325
|
+
...descriptor,
|
|
326
|
+
id: base64URLStringToBuffer(id),
|
|
327
|
+
transports: descriptor.transports
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function isValidDomain(hostname) {
|
|
331
|
+
return hostname === "localhost" || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname);
|
|
332
|
+
}
|
|
333
|
+
function identifyRegistrationError({ error, options }) {
|
|
334
|
+
const { publicKey } = options;
|
|
335
|
+
if (!publicKey) {
|
|
336
|
+
throw Error("options was missing required publicKey property");
|
|
337
|
+
}
|
|
338
|
+
if (error.name === "AbortError") {
|
|
339
|
+
if (options.signal instanceof AbortSignal) {
|
|
340
|
+
return new WebAuthnError({
|
|
341
|
+
message: "Registration ceremony was sent an abort signal",
|
|
342
|
+
code: "ERROR_CEREMONY_ABORTED",
|
|
343
|
+
cause: error
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
} else if (error.name === "ConstraintError") {
|
|
347
|
+
if (publicKey.authenticatorSelection?.requireResidentKey === true) {
|
|
348
|
+
return new WebAuthnError({
|
|
349
|
+
message: "Discoverable credentials were required but no available authenticator supported it",
|
|
350
|
+
code: "ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",
|
|
351
|
+
cause: error
|
|
352
|
+
});
|
|
353
|
+
} else if (options.mediation === "conditional" && publicKey.authenticatorSelection?.userVerification === "required") {
|
|
354
|
+
return new WebAuthnError({
|
|
355
|
+
message: "User verification was required during automatic registration but it could not be performed",
|
|
356
|
+
code: "ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",
|
|
357
|
+
cause: error
|
|
358
|
+
});
|
|
359
|
+
} else if (publicKey.authenticatorSelection?.userVerification === "required") {
|
|
360
|
+
return new WebAuthnError({
|
|
361
|
+
message: "User verification was required but no available authenticator supported it",
|
|
362
|
+
code: "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",
|
|
363
|
+
cause: error
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
} else if (error.name === "InvalidStateError") {
|
|
367
|
+
return new WebAuthnError({
|
|
368
|
+
message: "The authenticator was previously registered",
|
|
369
|
+
code: "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",
|
|
370
|
+
cause: error
|
|
371
|
+
});
|
|
372
|
+
} else if (error.name === "NotAllowedError") {
|
|
373
|
+
return new WebAuthnError({
|
|
374
|
+
message: error.message,
|
|
375
|
+
code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
|
|
376
|
+
cause: error
|
|
377
|
+
});
|
|
378
|
+
} else if (error.name === "NotSupportedError") {
|
|
379
|
+
const validPubKeyCredParams = publicKey.pubKeyCredParams.filter((param) => param.type === "public-key");
|
|
380
|
+
if (validPubKeyCredParams.length === 0) {
|
|
381
|
+
return new WebAuthnError({
|
|
382
|
+
message: 'No entry in pubKeyCredParams was of type "public-key"',
|
|
383
|
+
code: "ERROR_MALFORMED_PUBKEYCREDPARAMS",
|
|
384
|
+
cause: error
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return new WebAuthnError({
|
|
388
|
+
message: "No available authenticator supported any of the specified pubKeyCredParams algorithms",
|
|
389
|
+
code: "ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",
|
|
390
|
+
cause: error
|
|
391
|
+
});
|
|
392
|
+
} else if (error.name === "SecurityError") {
|
|
393
|
+
const effectiveDomain = window.location.hostname;
|
|
394
|
+
if (!isValidDomain(effectiveDomain)) {
|
|
395
|
+
return new WebAuthnError({
|
|
396
|
+
message: `${window.location.hostname} is an invalid domain`,
|
|
397
|
+
code: "ERROR_INVALID_DOMAIN",
|
|
398
|
+
cause: error
|
|
399
|
+
});
|
|
400
|
+
} else if (publicKey.rp.id !== effectiveDomain) {
|
|
401
|
+
return new WebAuthnError({
|
|
402
|
+
message: `The RP ID "${publicKey.rp.id}" is invalid for this domain`,
|
|
403
|
+
code: "ERROR_INVALID_RP_ID",
|
|
404
|
+
cause: error
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
} else if (error.name === "TypeError") {
|
|
408
|
+
if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) {
|
|
409
|
+
return new WebAuthnError({
|
|
410
|
+
message: "User ID was not between 1 and 64 characters",
|
|
411
|
+
code: "ERROR_INVALID_USER_ID_LENGTH",
|
|
412
|
+
cause: error
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
} else if (error.name === "UnknownError") {
|
|
416
|
+
return new WebAuthnError({
|
|
417
|
+
message: "The authenticator was unable to process the specified options, or could not create a new credential",
|
|
418
|
+
code: "ERROR_AUTHENTICATOR_GENERAL_ERROR",
|
|
419
|
+
cause: error
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return error;
|
|
423
|
+
}
|
|
424
|
+
function toAuthenticatorAttachment(attachment) {
|
|
425
|
+
if (!attachment) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (attachments.indexOf(attachment) < 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
return attachment;
|
|
432
|
+
}
|
|
433
|
+
async function startRegistration(options) {
|
|
434
|
+
const { optionsJSON, useAutoRegister = false } = options;
|
|
435
|
+
if (!browserSupportsWebAuthn()) {
|
|
436
|
+
throw new Error("WebAuthn is not supported in this browser");
|
|
437
|
+
}
|
|
438
|
+
const publicKey = {
|
|
439
|
+
...optionsJSON,
|
|
440
|
+
challenge: base64URLStringToBuffer(optionsJSON.challenge),
|
|
441
|
+
user: {
|
|
442
|
+
...optionsJSON.user,
|
|
443
|
+
id: base64URLStringToBuffer(optionsJSON.user.id)
|
|
444
|
+
},
|
|
445
|
+
excludeCredentials: optionsJSON.excludeCredentials?.map(toPublicKeyCredentialDescriptor)
|
|
446
|
+
};
|
|
447
|
+
const createOptions = {};
|
|
448
|
+
if (useAutoRegister) {
|
|
449
|
+
createOptions.mediation = "conditional";
|
|
450
|
+
}
|
|
451
|
+
createOptions.publicKey = publicKey;
|
|
452
|
+
createOptions.signal = WebAuthnAbortService.createNewAbortSignal();
|
|
453
|
+
let credential;
|
|
454
|
+
try {
|
|
455
|
+
credential = await navigator.credentials.create(createOptions);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
throw identifyRegistrationError({ error: err, options: createOptions });
|
|
458
|
+
}
|
|
459
|
+
if (!credential) {
|
|
460
|
+
throw new Error("Registration was not completed");
|
|
461
|
+
}
|
|
462
|
+
const { id, rawId, response, type } = credential;
|
|
463
|
+
let transports = void 0;
|
|
464
|
+
if (typeof response.getTransports === "function") {
|
|
465
|
+
transports = response.getTransports();
|
|
466
|
+
}
|
|
467
|
+
let responsePublicKeyAlgorithm = void 0;
|
|
468
|
+
if (typeof response.getPublicKeyAlgorithm === "function") {
|
|
469
|
+
try {
|
|
470
|
+
responsePublicKeyAlgorithm = response.getPublicKeyAlgorithm();
|
|
471
|
+
} catch (error) {
|
|
472
|
+
warnOnBrokenImplementation("getPublicKeyAlgorithm()", error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
let responsePublicKey = void 0;
|
|
476
|
+
if (typeof response.getPublicKey === "function") {
|
|
477
|
+
try {
|
|
478
|
+
const _publicKey = response.getPublicKey();
|
|
479
|
+
if (_publicKey !== null) {
|
|
480
|
+
responsePublicKey = bufferToBase64URLString(_publicKey);
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
warnOnBrokenImplementation("getPublicKey()", error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let responseAuthenticatorData;
|
|
487
|
+
if (typeof response.getAuthenticatorData === "function") {
|
|
488
|
+
try {
|
|
489
|
+
responseAuthenticatorData = bufferToBase64URLString(response.getAuthenticatorData());
|
|
490
|
+
} catch (error) {
|
|
491
|
+
warnOnBrokenImplementation("getAuthenticatorData()", error);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
id,
|
|
496
|
+
rawId: bufferToBase64URLString(rawId),
|
|
497
|
+
response: {
|
|
498
|
+
attestationObject: bufferToBase64URLString(response.attestationObject),
|
|
499
|
+
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
|
|
500
|
+
transports,
|
|
501
|
+
publicKeyAlgorithm: responsePublicKeyAlgorithm,
|
|
502
|
+
publicKey: responsePublicKey,
|
|
503
|
+
authenticatorData: responseAuthenticatorData
|
|
504
|
+
},
|
|
505
|
+
type,
|
|
506
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
507
|
+
authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment)
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function warnOnBrokenImplementation(methodName, cause) {
|
|
511
|
+
console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${methodName}. You should report this error to them.
|
|
512
|
+
`, cause);
|
|
513
|
+
}
|
|
514
|
+
function browserSupportsWebAuthnAutofill() {
|
|
515
|
+
if (!browserSupportsWebAuthn()) {
|
|
516
|
+
return new Promise((resolve) => resolve(false));
|
|
517
|
+
}
|
|
518
|
+
const globalPublicKeyCredential = window.PublicKeyCredential;
|
|
519
|
+
if (globalPublicKeyCredential.isConditionalMediationAvailable === void 0) {
|
|
520
|
+
return new Promise((resolve) => resolve(false));
|
|
521
|
+
}
|
|
522
|
+
return globalPublicKeyCredential.isConditionalMediationAvailable();
|
|
523
|
+
}
|
|
524
|
+
function identifyAuthenticationError({ error, options }) {
|
|
525
|
+
const { publicKey } = options;
|
|
526
|
+
if (!publicKey) {
|
|
527
|
+
throw Error("options was missing required publicKey property");
|
|
528
|
+
}
|
|
529
|
+
if (error.name === "AbortError") {
|
|
530
|
+
if (options.signal instanceof AbortSignal) {
|
|
531
|
+
return new WebAuthnError({
|
|
532
|
+
message: "Authentication ceremony was sent an abort signal",
|
|
533
|
+
code: "ERROR_CEREMONY_ABORTED",
|
|
534
|
+
cause: error
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
} else if (error.name === "NotAllowedError") {
|
|
538
|
+
return new WebAuthnError({
|
|
539
|
+
message: error.message,
|
|
540
|
+
code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
|
|
541
|
+
cause: error
|
|
542
|
+
});
|
|
543
|
+
} else if (error.name === "SecurityError") {
|
|
544
|
+
const effectiveDomain = window.location.hostname;
|
|
545
|
+
if (!isValidDomain(effectiveDomain)) {
|
|
546
|
+
return new WebAuthnError({
|
|
547
|
+
message: `${window.location.hostname} is an invalid domain`,
|
|
548
|
+
code: "ERROR_INVALID_DOMAIN",
|
|
549
|
+
cause: error
|
|
550
|
+
});
|
|
551
|
+
} else if (publicKey.rpId !== effectiveDomain) {
|
|
552
|
+
return new WebAuthnError({
|
|
553
|
+
message: `The RP ID "${publicKey.rpId}" is invalid for this domain`,
|
|
554
|
+
code: "ERROR_INVALID_RP_ID",
|
|
555
|
+
cause: error
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
} else if (error.name === "UnknownError") {
|
|
559
|
+
return new WebAuthnError({
|
|
560
|
+
message: "The authenticator was unable to process the specified options, or could not create a new assertion signature",
|
|
561
|
+
code: "ERROR_AUTHENTICATOR_GENERAL_ERROR",
|
|
562
|
+
cause: error
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return error;
|
|
566
|
+
}
|
|
567
|
+
async function startAuthentication(options) {
|
|
568
|
+
const { optionsJSON, useBrowserAutofill = false, verifyBrowserAutofillInput = true } = options;
|
|
569
|
+
if (!browserSupportsWebAuthn()) {
|
|
570
|
+
throw new Error("WebAuthn is not supported in this browser");
|
|
571
|
+
}
|
|
572
|
+
let allowCredentials;
|
|
573
|
+
if (optionsJSON.allowCredentials?.length !== 0) {
|
|
574
|
+
allowCredentials = optionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor);
|
|
575
|
+
}
|
|
576
|
+
const publicKey = {
|
|
577
|
+
...optionsJSON,
|
|
578
|
+
challenge: base64URLStringToBuffer(optionsJSON.challenge),
|
|
579
|
+
allowCredentials
|
|
580
|
+
};
|
|
581
|
+
const getOptions = {};
|
|
582
|
+
if (useBrowserAutofill) {
|
|
583
|
+
if (!await browserSupportsWebAuthnAutofill()) {
|
|
584
|
+
throw Error("Browser does not support WebAuthn autofill");
|
|
585
|
+
}
|
|
586
|
+
const eligibleInputs = document.querySelectorAll("input[autocomplete$='webauthn']");
|
|
587
|
+
if (eligibleInputs.length < 1 && verifyBrowserAutofillInput) {
|
|
588
|
+
throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');
|
|
589
|
+
}
|
|
590
|
+
getOptions.mediation = "conditional";
|
|
591
|
+
publicKey.allowCredentials = [];
|
|
592
|
+
}
|
|
593
|
+
getOptions.publicKey = publicKey;
|
|
594
|
+
getOptions.signal = WebAuthnAbortService.createNewAbortSignal();
|
|
595
|
+
let credential;
|
|
596
|
+
try {
|
|
597
|
+
credential = await navigator.credentials.get(getOptions);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
throw identifyAuthenticationError({ error: err, options: getOptions });
|
|
600
|
+
}
|
|
601
|
+
if (!credential) {
|
|
602
|
+
throw new Error("Authentication was not completed");
|
|
603
|
+
}
|
|
604
|
+
const { id, rawId, response, type } = credential;
|
|
605
|
+
let userHandle = void 0;
|
|
606
|
+
if (response.userHandle) {
|
|
607
|
+
userHandle = bufferToBase64URLString(response.userHandle);
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
id,
|
|
611
|
+
rawId: bufferToBase64URLString(rawId),
|
|
612
|
+
response: {
|
|
613
|
+
authenticatorData: bufferToBase64URLString(response.authenticatorData),
|
|
614
|
+
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
|
|
615
|
+
signature: bufferToBase64URLString(response.signature),
|
|
616
|
+
userHandle
|
|
617
|
+
},
|
|
618
|
+
type,
|
|
619
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
620
|
+
authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function platformAuthenticatorIsAvailable() {
|
|
624
|
+
if (!browserSupportsWebAuthn()) {
|
|
625
|
+
return new Promise((resolve) => resolve(false));
|
|
626
|
+
}
|
|
627
|
+
return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
628
|
+
}
|
|
629
|
+
var WebAuthnError, BaseWebAuthnAbortService, WebAuthnAbortService, attachments;
|
|
630
|
+
var init_bundle = __esm({
|
|
631
|
+
"../../node_modules/@simplewebauthn/browser/dist/bundle/index.js"() {
|
|
632
|
+
"use strict";
|
|
633
|
+
WebAuthnError = class extends Error {
|
|
634
|
+
constructor({ message, code, cause, name }) {
|
|
635
|
+
super(message, { cause });
|
|
636
|
+
this.name = name ?? cause.name;
|
|
637
|
+
this.code = code;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
BaseWebAuthnAbortService = class {
|
|
641
|
+
createNewAbortSignal() {
|
|
642
|
+
if (this.controller) {
|
|
643
|
+
const abortError = new Error("Cancelling existing WebAuthn API call for new one");
|
|
644
|
+
abortError.name = "AbortError";
|
|
645
|
+
this.controller.abort(abortError);
|
|
646
|
+
}
|
|
647
|
+
const newController = new AbortController();
|
|
648
|
+
this.controller = newController;
|
|
649
|
+
return newController.signal;
|
|
650
|
+
}
|
|
651
|
+
cancelCeremony() {
|
|
652
|
+
if (this.controller) {
|
|
653
|
+
const abortError = new Error("Manually cancelling existing WebAuthn API call");
|
|
654
|
+
abortError.name = "AbortError";
|
|
655
|
+
this.controller.abort(abortError);
|
|
656
|
+
this.controller = void 0;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
WebAuthnAbortService = new BaseWebAuthnAbortService();
|
|
661
|
+
attachments = ["cross-platform", "platform"];
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// src/browser/reverify.ts
|
|
666
|
+
var reverify_exports = {};
|
|
667
|
+
__export(reverify_exports, {
|
|
668
|
+
PRIOR_SESSION_STORAGE_KEY: () => PRIOR_SESSION_STORAGE_KEY,
|
|
669
|
+
enterImpersonation: () => enterImpersonation,
|
|
670
|
+
exitImpersonation: () => exitImpersonation,
|
|
671
|
+
reverify: () => reverify,
|
|
672
|
+
withReverification: () => withReverification
|
|
673
|
+
});
|
|
674
|
+
function enterImpersonation(manager, actorAccessToken) {
|
|
675
|
+
if (typeof window === "undefined") return;
|
|
676
|
+
const current = manager.getSnapshot().accessToken;
|
|
677
|
+
if (current) {
|
|
678
|
+
const envelope = { accessToken: current, savedAt: Date.now() };
|
|
679
|
+
try {
|
|
680
|
+
window.sessionStorage.setItem(PRIOR_SESSION_STORAGE_KEY, JSON.stringify(envelope));
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
manager.applyAccessToken(actorAccessToken);
|
|
685
|
+
}
|
|
686
|
+
function exitImpersonation(manager) {
|
|
687
|
+
if (typeof window === "undefined") return false;
|
|
688
|
+
let raw = null;
|
|
689
|
+
try {
|
|
690
|
+
raw = window.sessionStorage.getItem(PRIOR_SESSION_STORAGE_KEY);
|
|
691
|
+
} catch {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
if (!raw) return false;
|
|
695
|
+
try {
|
|
696
|
+
const envelope = JSON.parse(raw);
|
|
697
|
+
if (!envelope?.accessToken) return false;
|
|
698
|
+
manager.applyAccessToken(envelope.accessToken);
|
|
699
|
+
return true;
|
|
700
|
+
} catch {
|
|
701
|
+
return false;
|
|
702
|
+
} finally {
|
|
703
|
+
try {
|
|
704
|
+
window.sessionStorage.removeItem(PRIOR_SESSION_STORAGE_KEY);
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function reverify(manager, input, options = {}) {
|
|
710
|
+
if (input.level === "password" && !input.password) {
|
|
711
|
+
throw new IQAuthError("MISSING_PASSWORD", "password is required for level=password");
|
|
712
|
+
}
|
|
713
|
+
if (input.level === "mfa" && !input.totp) {
|
|
714
|
+
throw new IQAuthError("MISSING_CODE", "totp code is required for level=mfa");
|
|
715
|
+
}
|
|
716
|
+
const issuer = manager.issuerUrl.replace(/\/$/, "");
|
|
717
|
+
const url2 = `${issuer}${options.path ?? DEFAULT_REVERIFY_PATH}`;
|
|
718
|
+
const res = await manager.fetch(url2, {
|
|
719
|
+
method: "POST",
|
|
720
|
+
headers: { "Content-Type": "application/json" },
|
|
721
|
+
body: JSON.stringify(input)
|
|
722
|
+
});
|
|
723
|
+
let payload = null;
|
|
724
|
+
try {
|
|
725
|
+
payload = await res.json();
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
if (!res.ok || !payload?.success) {
|
|
729
|
+
const code = payload?.error?.code ?? "REVERIFICATION_FAILED";
|
|
730
|
+
const message = payload?.error?.message ?? `reverification failed (${res.status})`;
|
|
731
|
+
throw new IQAuthError(code, message);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
token: payload.data.reverificationToken,
|
|
735
|
+
level: payload.data.level,
|
|
736
|
+
expiresAt: new Date(payload.data.expiresAt)
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function withReverification(manager, token) {
|
|
740
|
+
let used = false;
|
|
741
|
+
return async (input, init = {}) => {
|
|
742
|
+
if (used) throw new IQAuthError("REVERIFICATION_USED", "Reverification token already consumed");
|
|
743
|
+
used = true;
|
|
744
|
+
const headers2 = new Headers(init.headers);
|
|
745
|
+
headers2.set("X-Reverification-Token", token);
|
|
746
|
+
return manager.fetch(input, { ...init, headers: headers2 });
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
var PRIOR_SESSION_STORAGE_KEY, DEFAULT_REVERIFY_PATH;
|
|
750
|
+
var init_reverify = __esm({
|
|
751
|
+
"src/browser/reverify.ts"() {
|
|
752
|
+
"use strict";
|
|
753
|
+
init_errors();
|
|
754
|
+
PRIOR_SESSION_STORAGE_KEY = "iqauth_prior_admin_session";
|
|
755
|
+
DEFAULT_REVERIFY_PATH = "/api/v1/auth/reverify";
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// src/react.ts
|
|
760
|
+
var react_exports = {};
|
|
761
|
+
__export(react_exports, {
|
|
762
|
+
AuthCallback: () => AuthCallback,
|
|
763
|
+
CreateOrganization: () => CreateOrganization,
|
|
764
|
+
IQAuthLoaded: () => IQAuthLoaded,
|
|
765
|
+
IQAuthLoading: () => IQAuthLoading,
|
|
766
|
+
IQAuthProvider: () => IQAuthProvider,
|
|
767
|
+
IQAuthReturnToBouncer: () => IQAuthReturnToBouncer,
|
|
768
|
+
ImpersonationBanner: () => ImpersonationBanner,
|
|
769
|
+
LinkedAccounts: () => LinkedAccounts,
|
|
770
|
+
MagicLinkSignInForm: () => MagicLinkSignInForm,
|
|
771
|
+
MultisessionAppSupport: () => MultisessionAppSupport,
|
|
772
|
+
OrganizationList: () => OrganizationList,
|
|
773
|
+
OrganizationProfile: () => OrganizationProfile,
|
|
774
|
+
OrganizationSwitcher: () => OrganizationSwitcher,
|
|
775
|
+
PasskeySignInButton: () => PasskeySignInButton,
|
|
776
|
+
Protect: () => Protect,
|
|
777
|
+
RedirectToSignIn: () => RedirectToSignIn,
|
|
778
|
+
RedirectToSignedIn: () => RedirectToSignedIn,
|
|
779
|
+
SignIn: () => SignIn,
|
|
780
|
+
SignUp: () => SignUp,
|
|
781
|
+
SignedIn: () => SignedIn,
|
|
782
|
+
SignedOut: () => SignedOut,
|
|
783
|
+
UserButton: () => UserButton,
|
|
784
|
+
UserProfile: () => UserProfile,
|
|
785
|
+
Waitlist: () => Waitlist,
|
|
786
|
+
__version__: () => __version__,
|
|
787
|
+
isReturnToAllowed: () => isReturnToAllowed,
|
|
788
|
+
isSilentSsoEligible: () => isSilentSsoEligible,
|
|
789
|
+
preflightReturnTo: () => preflightReturnTo,
|
|
790
|
+
revokeSession: () => revokeSession,
|
|
791
|
+
sanitizeBrandCss: () => sanitizeBrandCss,
|
|
792
|
+
sanitizeReturnTo: () => sanitizeReturnTo,
|
|
793
|
+
slugify: () => slugify,
|
|
794
|
+
useAccountList: () => useAccountList,
|
|
795
|
+
useAccountSwitcher: () => useAccountSwitcher,
|
|
796
|
+
useAuth: () => useAuth,
|
|
797
|
+
useAuthFetch: () => useAuthFetch,
|
|
798
|
+
useIQAuthSignInContext: () => useIQAuthSignInContext,
|
|
799
|
+
useImpersonation: () => useImpersonation,
|
|
800
|
+
useLinkedIdentities: () => useLinkedIdentities,
|
|
801
|
+
useLocale: () => useLocale,
|
|
802
|
+
useMagicLink: () => useMagicLink,
|
|
803
|
+
useOrganization: () => useOrganization,
|
|
804
|
+
usePasskey: () => usePasskey,
|
|
805
|
+
useResolvedSdkBranding: () => useResolvedSdkBranding,
|
|
806
|
+
useReturnTo: () => useReturnTo,
|
|
807
|
+
useReverification: () => useReverification,
|
|
808
|
+
useSession: () => useSession,
|
|
809
|
+
useSessionList: () => useSessionList,
|
|
810
|
+
useT: () => useT,
|
|
811
|
+
useUser: () => useUser
|
|
812
|
+
});
|
|
813
|
+
module.exports = __toCommonJS(react_exports);
|
|
814
|
+
|
|
815
|
+
// src/react/index.tsx
|
|
816
|
+
var import_react = require("react");
|
|
817
|
+
|
|
818
|
+
// src/browser/sessionManager.ts
|
|
819
|
+
init_errors();
|
|
820
|
+
|
|
821
|
+
// src/publishableKey.ts
|
|
822
|
+
init_errors();
|
|
823
|
+
function b64urlDecode(input) {
|
|
824
|
+
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
825
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
826
|
+
if (typeof atob === "function") {
|
|
827
|
+
const bin = atob(normalized);
|
|
828
|
+
const bytes = new Uint8Array(bin.length);
|
|
829
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
830
|
+
return new TextDecoder().decode(bytes);
|
|
831
|
+
}
|
|
832
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
833
|
+
return Buffer2.from(normalized, "base64").toString("utf8");
|
|
834
|
+
}
|
|
835
|
+
function isValidIssuerUrl(iss) {
|
|
836
|
+
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
837
|
+
if (!iss.startsWith("http://") && !iss.startsWith("https://")) return false;
|
|
838
|
+
try {
|
|
839
|
+
const u = new URL(iss);
|
|
840
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
841
|
+
if (!u.hostname) return false;
|
|
842
|
+
return true;
|
|
843
|
+
} catch {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function assertPublishableKey(raw, opts) {
|
|
848
|
+
const ctx = opts?.context ? `${opts.context}: ` : "";
|
|
849
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
850
|
+
throw new IQAuthError(
|
|
851
|
+
"CONFIG_INVALID",
|
|
852
|
+
`${ctx}IQAuth publishable key is missing. Set IQAUTH_PUBLISHABLE_KEY (or pass publishableKey) to a pk_test_\u2026 or pk_live_\u2026 value from the IQAuth admin console.`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const shapeMatch = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
856
|
+
if (!shapeMatch) {
|
|
857
|
+
throw new IQAuthError(
|
|
858
|
+
"CONFIG_INVALID",
|
|
859
|
+
`${ctx}IQAuth publishable key is malformed (got ${raw.slice(0, 12)}\u2026). Expected pk_test_\u2026 or pk_live_\u2026; regenerate the key from the IQAuth admin console.`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
let decoded;
|
|
863
|
+
try {
|
|
864
|
+
decoded = JSON.parse(b64urlDecode(shapeMatch[2]));
|
|
865
|
+
} catch {
|
|
866
|
+
throw new IQAuthError(
|
|
867
|
+
"CONFIG_INVALID",
|
|
868
|
+
`${ctx}IQAuth publishable key payload is not valid base64url JSON. Regenerate the key from the IQAuth admin console.`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
if (!isPublishableKeyPayload(decoded)) {
|
|
872
|
+
throw new IQAuthError(
|
|
873
|
+
"CONFIG_INVALID",
|
|
874
|
+
`${ctx}IQAuth publishable key payload is missing required fields {iss, appId, tenantId, kid}. Regenerate the key from the IQAuth admin console.`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (!isValidIssuerUrl(decoded.iss)) {
|
|
878
|
+
throw new IQAuthError(
|
|
879
|
+
"CONFIG_INVALID",
|
|
880
|
+
`${ctx}IQAuth publishable key encodes an invalid issuer (iss=${JSON.stringify(decoded.iss)}). Expected a fully-qualified URL like "https://auth.example.com" (scheme required). Regenerate the key from the IQAuth admin console \u2014 the new key will encode a valid issuer URL.`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
return { mode: shapeMatch[1], iss: decoded.iss, appId: decoded.appId, tenantId: decoded.tenantId, kid: decoded.kid, raw };
|
|
884
|
+
}
|
|
885
|
+
function isPublishableKeyPayload(value) {
|
|
886
|
+
if (!value || typeof value !== "object") return false;
|
|
887
|
+
const v = value;
|
|
888
|
+
return typeof v.iss === "string" && typeof v.appId === "string" && typeof v.tenantId === "string" && typeof v.kid === "string";
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/browser/sessionManager.ts
|
|
892
|
+
init_storage();
|
|
893
|
+
var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
|
|
894
|
+
var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
|
|
895
|
+
async function readAuthErrorCode(res) {
|
|
896
|
+
try {
|
|
897
|
+
const cloned = res.clone();
|
|
898
|
+
const data = await cloned.json().catch(() => null);
|
|
899
|
+
if (!data || typeof data !== "object") return null;
|
|
900
|
+
const err = data.error;
|
|
901
|
+
if (err && typeof err.code === "string") {
|
|
902
|
+
return { code: err.code, message: typeof err.message === "string" ? err.message : void 0 };
|
|
903
|
+
}
|
|
904
|
+
return null;
|
|
905
|
+
} catch {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function decodeClaims(token) {
|
|
910
|
+
try {
|
|
911
|
+
const parts = token.split(".");
|
|
912
|
+
if (parts.length !== 3) return null;
|
|
913
|
+
const json = typeof atob === "function" ? decodeURIComponent(
|
|
914
|
+
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
915
|
+
) : Buffer.from(parts[1], "base64url").toString("utf8");
|
|
916
|
+
return JSON.parse(json);
|
|
917
|
+
} catch {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function claimsToSessionUser(claims) {
|
|
922
|
+
if (!claims) return null;
|
|
923
|
+
return {
|
|
924
|
+
sub: claims.sub,
|
|
925
|
+
email: claims.email,
|
|
926
|
+
name: claims.name,
|
|
927
|
+
tenantId: claims.tenantId,
|
|
928
|
+
vendorId: claims.vendorId,
|
|
929
|
+
roles: claims.roles ?? [],
|
|
930
|
+
entitlements: claims.entitlements ?? []
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
var EMPTY = {
|
|
934
|
+
status: "loading",
|
|
935
|
+
accessToken: null,
|
|
936
|
+
user: null,
|
|
937
|
+
claims: null,
|
|
938
|
+
tenantId: null,
|
|
939
|
+
error: null,
|
|
940
|
+
version: 0
|
|
941
|
+
};
|
|
942
|
+
function defaultCookieStore(name = REFRESH_COOKIE) {
|
|
943
|
+
return {
|
|
944
|
+
read: () => getCookie(name),
|
|
945
|
+
write: (token) => setCookie(name, token, { maxAgeSeconds: 60 * 60 * 24 * 30 }),
|
|
946
|
+
clear: () => clearCookie(name)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
var NO_OP_STORE = {
|
|
950
|
+
read: () => null,
|
|
951
|
+
write: () => void 0,
|
|
952
|
+
clear: () => void 0
|
|
953
|
+
};
|
|
954
|
+
var SessionManager = class {
|
|
955
|
+
constructor(options) {
|
|
956
|
+
this.snapshot = { ...EMPTY };
|
|
957
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
958
|
+
this.refreshPromise = null;
|
|
959
|
+
this.channel = null;
|
|
960
|
+
this.proactiveTimer = null;
|
|
961
|
+
this.bootstrapped = false;
|
|
962
|
+
/** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
|
|
963
|
+
this.remoteRefreshWaiters = [];
|
|
964
|
+
/** Active claims by other tabs (keyed by source tabId). */
|
|
965
|
+
this.foreignClaim = null;
|
|
966
|
+
const parsed = assertPublishableKey(options.publishableKey, { context: "@iqauth/sdk/browser SessionManager" });
|
|
967
|
+
this.key = parsed;
|
|
968
|
+
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
969
|
+
this.issuer = inferred.replace(/\/+$/, "");
|
|
970
|
+
this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
971
|
+
this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
|
|
972
|
+
this.useCookies = options.useCookies ?? true;
|
|
973
|
+
this.serverManagedSession = options.serverManagedSession ?? false;
|
|
974
|
+
this.proactiveRefresh = this.serverManagedSession ? false : options.proactiveRefresh ?? true;
|
|
975
|
+
this.refreshCookieName = options.cookieNames?.refresh ?? REFRESH_COOKIE;
|
|
976
|
+
this.tokenStore = options.tokenStore ?? (this.serverManagedSession ? NO_OP_STORE : this.useCookies ? defaultCookieStore(this.refreshCookieName) : NO_OP_STORE);
|
|
977
|
+
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
978
|
+
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
979
|
+
throw new Error("global fetch is not available; pass fetchImpl");
|
|
980
|
+
}));
|
|
981
|
+
this.tabId = Math.random().toString(36).slice(2);
|
|
982
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
983
|
+
const name = options.channelName ?? `iqauth.${parsed.appId}`;
|
|
984
|
+
try {
|
|
985
|
+
this.channel = new BroadcastChannel(name);
|
|
986
|
+
this.channel.onmessage = (ev) => this.onBroadcast(ev.data);
|
|
987
|
+
} catch {
|
|
988
|
+
this.channel = null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
get publishableKey() {
|
|
993
|
+
return this.key;
|
|
994
|
+
}
|
|
995
|
+
get appKey() {
|
|
996
|
+
return this.key.appId;
|
|
997
|
+
}
|
|
998
|
+
get tenantIdFromKey() {
|
|
999
|
+
return this.key.tenantId;
|
|
1000
|
+
}
|
|
1001
|
+
get issuerUrl() {
|
|
1002
|
+
return this.issuer;
|
|
1003
|
+
}
|
|
1004
|
+
/** Cookie name the SDK uses for the refresh token (overridable via `cookieNames.refresh`). */
|
|
1005
|
+
get refreshCookie() {
|
|
1006
|
+
return this.refreshCookieName;
|
|
1007
|
+
}
|
|
1008
|
+
getSnapshot() {
|
|
289
1009
|
return this.snapshot;
|
|
290
1010
|
}
|
|
291
1011
|
subscribe(listener) {
|
|
@@ -391,7 +1111,7 @@ var SessionManager = class {
|
|
|
391
1111
|
return false;
|
|
392
1112
|
}
|
|
393
1113
|
if (data.refreshToken) {
|
|
394
|
-
await Promise.resolve(this.tokenStore.write(data.refreshToken));
|
|
1114
|
+
await Promise.resolve(this.tokenStore.write(data.refreshToken, { claims: decodeClaims(data.accessToken) }));
|
|
395
1115
|
}
|
|
396
1116
|
this.applyAccessToken(data.accessToken);
|
|
397
1117
|
this.broadcast("session:refresh");
|
|
@@ -435,7 +1155,7 @@ var SessionManager = class {
|
|
|
435
1155
|
const claims = decodeClaims(accessToken);
|
|
436
1156
|
const user = claimsToSessionUser(claims);
|
|
437
1157
|
if (refreshToken) {
|
|
438
|
-
void Promise.resolve(this.tokenStore.write(refreshToken));
|
|
1158
|
+
void Promise.resolve(this.tokenStore.write(refreshToken, { claims }));
|
|
439
1159
|
}
|
|
440
1160
|
this.update({
|
|
441
1161
|
status: user ? "authenticated" : "unauthenticated",
|
|
@@ -469,33 +1189,39 @@ var SessionManager = class {
|
|
|
469
1189
|
*/
|
|
470
1190
|
async fetch(input, init = {}) {
|
|
471
1191
|
const exec = async (token2) => {
|
|
472
|
-
const
|
|
473
|
-
if (token2)
|
|
1192
|
+
const headers2 = new Headers(init.headers || {});
|
|
1193
|
+
if (token2) headers2.set("Authorization", `Bearer ${token2}`);
|
|
474
1194
|
return this.fetchImpl(input, {
|
|
475
1195
|
...init,
|
|
476
|
-
headers,
|
|
1196
|
+
headers: headers2,
|
|
477
1197
|
credentials: init.credentials ?? "include"
|
|
478
1198
|
});
|
|
479
1199
|
};
|
|
480
1200
|
let token = await this.getToken();
|
|
481
1201
|
let res = await exec(token);
|
|
482
1202
|
if (res.status !== 401) return res;
|
|
1203
|
+
const initialErr = await readAuthErrorCode(res);
|
|
1204
|
+
if (initialErr && (initialErr.code === "SESSION_EXPIRED_INACTIVITY" || initialErr.code === "SESSION_EXPIRED_MAX_DURATION" || initialErr.code === "SESSION_INVALID")) {
|
|
1205
|
+
this.signOutLocal("unauthenticated");
|
|
1206
|
+
throw new IQAuthError(initialErr.code, initialErr.message || "Session expired", 401);
|
|
1207
|
+
}
|
|
483
1208
|
const refreshed = await this.refresh();
|
|
484
1209
|
if (!refreshed) {
|
|
485
1210
|
this.signOutLocal("unauthenticated");
|
|
486
1211
|
throw new IQAuthError(
|
|
487
|
-
"TOKEN_EXPIRED",
|
|
488
|
-
"Session refresh failed; user must sign in again",
|
|
1212
|
+
initialErr?.code || "TOKEN_EXPIRED",
|
|
1213
|
+
initialErr?.message || "Session refresh failed; user must sign in again",
|
|
489
1214
|
401
|
|
490
1215
|
);
|
|
491
1216
|
}
|
|
492
1217
|
token = this.snapshot.accessToken;
|
|
493
1218
|
res = await exec(token);
|
|
494
1219
|
if (res.status === 401) {
|
|
1220
|
+
const secondErr = await readAuthErrorCode(res);
|
|
495
1221
|
this.signOutLocal("unauthenticated");
|
|
496
1222
|
throw new IQAuthError(
|
|
497
|
-
"TOKEN_EXPIRED",
|
|
498
|
-
"Authenticated request failed twice with 401; aborting",
|
|
1223
|
+
secondErr?.code || "TOKEN_EXPIRED",
|
|
1224
|
+
secondErr?.message || "Authenticated request failed twice with 401; aborting",
|
|
499
1225
|
401
|
|
500
1226
|
);
|
|
501
1227
|
}
|
|
@@ -523,6 +1249,14 @@ var SessionManager = class {
|
|
|
523
1249
|
});
|
|
524
1250
|
this.broadcast("session:signout");
|
|
525
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Replace the refresh-token store at runtime. Used by the F22
|
|
1254
|
+
* `<MultisessionAppSupport>` wrapper to swap in a `MultiAccountTokenStore`
|
|
1255
|
+
* after the manager has already been constructed by `<IQAuthProvider>`.
|
|
1256
|
+
*/
|
|
1257
|
+
setTokenStore(store) {
|
|
1258
|
+
this.tokenStore = store;
|
|
1259
|
+
}
|
|
526
1260
|
destroy() {
|
|
527
1261
|
if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
|
|
528
1262
|
this.proactiveTimer = null;
|
|
@@ -589,190 +1323,518 @@ var SessionManager = class {
|
|
|
589
1323
|
this.foreignClaim = null;
|
|
590
1324
|
return;
|
|
591
1325
|
}
|
|
592
|
-
if (env.type === "session:signout") {
|
|
593
|
-
this.update({
|
|
594
|
-
status: "unauthenticated",
|
|
595
|
-
accessToken: null,
|
|
596
|
-
user: null,
|
|
597
|
-
claims: null,
|
|
598
|
-
tenantId: null,
|
|
599
|
-
error: null,
|
|
600
|
-
version: this.snapshot.version + 1
|
|
601
|
-
});
|
|
1326
|
+
if (env.type === "session:signout") {
|
|
1327
|
+
this.update({
|
|
1328
|
+
status: "unauthenticated",
|
|
1329
|
+
accessToken: null,
|
|
1330
|
+
user: null,
|
|
1331
|
+
claims: null,
|
|
1332
|
+
tenantId: null,
|
|
1333
|
+
error: null,
|
|
1334
|
+
version: this.snapshot.version + 1
|
|
1335
|
+
});
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
|
|
1339
|
+
this.update({
|
|
1340
|
+
...env.payload,
|
|
1341
|
+
version: Math.max(this.snapshot.version, env.payload.version) + 1
|
|
1342
|
+
});
|
|
1343
|
+
if (this.remoteRefreshWaiters.length) {
|
|
1344
|
+
const waiters = this.remoteRefreshWaiters;
|
|
1345
|
+
this.remoteRefreshWaiters = [];
|
|
1346
|
+
for (const w of waiters) w(true);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/react/index.tsx
|
|
1353
|
+
init_signIn();
|
|
1354
|
+
|
|
1355
|
+
// src/browser/accountRegistry.ts
|
|
1356
|
+
init_storage();
|
|
1357
|
+
var STORAGE_PREFIX = "iqauth.accounts.";
|
|
1358
|
+
var COOKIE_PREFIX = "iqauth_rt_";
|
|
1359
|
+
var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
|
1360
|
+
function isBrowser2() {
|
|
1361
|
+
return typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
1362
|
+
}
|
|
1363
|
+
function storageKey(appId) {
|
|
1364
|
+
return STORAGE_PREFIX + appId;
|
|
1365
|
+
}
|
|
1366
|
+
function readState(appId) {
|
|
1367
|
+
if (!isBrowser2()) return { accounts: [], activeAccountId: null };
|
|
1368
|
+
try {
|
|
1369
|
+
const raw = localStorage.getItem(storageKey(appId));
|
|
1370
|
+
if (!raw) return { accounts: [], activeAccountId: null };
|
|
1371
|
+
const parsed = JSON.parse(raw);
|
|
1372
|
+
return {
|
|
1373
|
+
accounts: Array.isArray(parsed.accounts) ? parsed.accounts : [],
|
|
1374
|
+
activeAccountId: parsed.activeAccountId ?? null
|
|
1375
|
+
};
|
|
1376
|
+
} catch {
|
|
1377
|
+
return { accounts: [], activeAccountId: null };
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function writeState(appId, state) {
|
|
1381
|
+
if (!isBrowser2()) return;
|
|
1382
|
+
try {
|
|
1383
|
+
localStorage.setItem(storageKey(appId), JSON.stringify(state));
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
var AccountRegistry = class {
|
|
1388
|
+
constructor(appId) {
|
|
1389
|
+
this.appId = appId;
|
|
1390
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1391
|
+
this.storageHandler = null;
|
|
1392
|
+
if (isBrowser2()) {
|
|
1393
|
+
this.storageHandler = (ev) => {
|
|
1394
|
+
if (ev.key === storageKey(this.appId)) {
|
|
1395
|
+
for (const l of this.listeners) l();
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
window.addEventListener("storage", this.storageHandler);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
destroy() {
|
|
1402
|
+
if (this.storageHandler && isBrowser2()) {
|
|
1403
|
+
window.removeEventListener("storage", this.storageHandler);
|
|
1404
|
+
}
|
|
1405
|
+
this.listeners.clear();
|
|
1406
|
+
}
|
|
1407
|
+
list() {
|
|
1408
|
+
return readState(this.appId).accounts.slice();
|
|
1409
|
+
}
|
|
1410
|
+
active() {
|
|
1411
|
+
return readState(this.appId).activeAccountId;
|
|
1412
|
+
}
|
|
1413
|
+
get(accountId) {
|
|
1414
|
+
return readState(this.appId).accounts.find((a) => a.accountId === accountId) ?? null;
|
|
1415
|
+
}
|
|
1416
|
+
upsert(rec) {
|
|
1417
|
+
const state = readState(this.appId);
|
|
1418
|
+
const idx = state.accounts.findIndex((a) => a.accountId === rec.accountId);
|
|
1419
|
+
if (idx >= 0) state.accounts[idx] = { ...state.accounts[idx], ...rec };
|
|
1420
|
+
else state.accounts.push(rec);
|
|
1421
|
+
writeState(this.appId, state);
|
|
1422
|
+
this.notify();
|
|
1423
|
+
}
|
|
1424
|
+
setActive(accountId) {
|
|
1425
|
+
const state = readState(this.appId);
|
|
1426
|
+
state.activeAccountId = accountId;
|
|
1427
|
+
writeState(this.appId, state);
|
|
1428
|
+
this.notify();
|
|
1429
|
+
}
|
|
1430
|
+
remove(accountId) {
|
|
1431
|
+
const state = readState(this.appId);
|
|
1432
|
+
state.accounts = state.accounts.filter((a) => a.accountId !== accountId);
|
|
1433
|
+
if (state.activeAccountId === accountId) state.activeAccountId = state.accounts[0]?.accountId ?? null;
|
|
1434
|
+
writeState(this.appId, state);
|
|
1435
|
+
clearCookie(COOKIE_PREFIX + accountId);
|
|
1436
|
+
this.notify();
|
|
1437
|
+
}
|
|
1438
|
+
subscribe(listener) {
|
|
1439
|
+
this.listeners.add(listener);
|
|
1440
|
+
return () => this.listeners.delete(listener);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Read the refresh token for a specific account from its per-account
|
|
1444
|
+
* cookie. Used by `MultiAccountTokenStore.read()`.
|
|
1445
|
+
*/
|
|
1446
|
+
readRefreshToken(accountId) {
|
|
1447
|
+
return getCookie(COOKIE_PREFIX + accountId);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Persist a refresh token to the per-account cookie. Caller is the
|
|
1451
|
+
* SessionManager via `MultiAccountTokenStore.write()`.
|
|
1452
|
+
*/
|
|
1453
|
+
writeRefreshToken(accountId, token, opts = {}) {
|
|
1454
|
+
setCookie(COOKIE_PREFIX + accountId, token, { maxAgeSeconds: COOKIE_MAX_AGE, ...opts });
|
|
1455
|
+
}
|
|
1456
|
+
clearRefreshToken(accountId) {
|
|
1457
|
+
clearCookie(COOKIE_PREFIX + accountId);
|
|
1458
|
+
}
|
|
1459
|
+
notify() {
|
|
1460
|
+
for (const l of this.listeners) l();
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
var MultiAccountTokenStore = class {
|
|
1464
|
+
constructor(registry, fallback) {
|
|
1465
|
+
this.registry = registry;
|
|
1466
|
+
this.fallback = fallback;
|
|
1467
|
+
}
|
|
1468
|
+
read() {
|
|
1469
|
+
const id = this.registry.active();
|
|
1470
|
+
if (id) return this.registry.readRefreshToken(id);
|
|
1471
|
+
return this.fallback.read();
|
|
1472
|
+
}
|
|
1473
|
+
write(token, ctx) {
|
|
1474
|
+
let id = this.registry.active();
|
|
1475
|
+
if (!id) {
|
|
1476
|
+
const sub = ctx?.claims?.sub;
|
|
1477
|
+
if (sub) {
|
|
1478
|
+
id = sub;
|
|
1479
|
+
this.registry.setActive(sub);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (id) {
|
|
1483
|
+
this.registry.writeRefreshToken(id, token);
|
|
602
1484
|
return;
|
|
603
1485
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
this.remoteRefreshWaiters = [];
|
|
612
|
-
for (const w of waiters) w(true);
|
|
613
|
-
}
|
|
1486
|
+
return this.fallback.write(token);
|
|
1487
|
+
}
|
|
1488
|
+
clear() {
|
|
1489
|
+
const id = this.registry.active();
|
|
1490
|
+
if (id) {
|
|
1491
|
+
this.registry.clearRefreshToken(id);
|
|
1492
|
+
return;
|
|
614
1493
|
}
|
|
1494
|
+
return this.fallback.clear();
|
|
615
1495
|
}
|
|
616
1496
|
};
|
|
617
1497
|
|
|
618
|
-
// src/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1498
|
+
// src/locales/enUS.ts
|
|
1499
|
+
var enUS = {
|
|
1500
|
+
locale: "en-US",
|
|
1501
|
+
"common.loading": "Loading\u2026",
|
|
1502
|
+
"common.submitting": "Submitting\u2026",
|
|
1503
|
+
"common.cancel": "Cancel",
|
|
1504
|
+
"common.continue": "Continue",
|
|
1505
|
+
"common.back": "Back",
|
|
1506
|
+
"common.close": "Close",
|
|
1507
|
+
"common.save": "Save",
|
|
1508
|
+
"common.saving": "Saving\u2026",
|
|
1509
|
+
"common.delete": "Delete",
|
|
1510
|
+
"common.confirm": "Confirm",
|
|
1511
|
+
"common.email": "Email",
|
|
1512
|
+
"common.password": "Password",
|
|
1513
|
+
"common.name": "Name",
|
|
1514
|
+
"common.or": "or",
|
|
1515
|
+
"common.required": "Required",
|
|
1516
|
+
"common.optional": "Optional",
|
|
1517
|
+
"common.retry": "Try again",
|
|
1518
|
+
"signIn.title": "Sign in",
|
|
1519
|
+
"signIn.subtitle": "Welcome back",
|
|
1520
|
+
"signIn.titleWithApp": "Sign in to {appName}",
|
|
1521
|
+
"signIn.emailLabel": "Email",
|
|
1522
|
+
"signIn.emailPlaceholder": "you@company.com",
|
|
1523
|
+
"signIn.passwordLabel": "Password",
|
|
1524
|
+
"signIn.passwordPlaceholder": "Your password",
|
|
1525
|
+
"signIn.submit": "Sign in",
|
|
1526
|
+
"signIn.submitting": "Signing in\u2026",
|
|
1527
|
+
"signIn.continueWithGoogle": "Continue with Google",
|
|
1528
|
+
"signIn.continueWithMagicLink": "Email me a sign-in link",
|
|
1529
|
+
"signIn.continueWithPasskey": "Sign in with passkey",
|
|
1530
|
+
"signIn.forgotPassword": "Forgot password?",
|
|
1531
|
+
"signIn.noAccount": "Don't have an account?",
|
|
1532
|
+
"signIn.signUp": "Sign up",
|
|
1533
|
+
"signIn.resumingSession": "Signing you in\u2026",
|
|
1534
|
+
"signIn.useDifferentAccount": "Use a different account",
|
|
1535
|
+
"signIn.selectTenant": "Choose an organization",
|
|
1536
|
+
"signIn.selectTenantSubtitle": "You belong to multiple organizations. Pick one to continue.",
|
|
1537
|
+
"signIn.dividerOr": "or",
|
|
1538
|
+
"signIn.preparingExperience": "Preparing your sign-in experience.",
|
|
1539
|
+
"signIn.applicationUnavailable": "Application unavailable",
|
|
1540
|
+
"signIn.applicationNotFound": "Application not found",
|
|
1541
|
+
"signIn.invalidRedirect": "Invalid redirect",
|
|
1542
|
+
"signIn.returnUrlNotRegistered": "The return URL is not registered for {appName}. Add the origin to the app's allowed origins.",
|
|
1543
|
+
"signIn.welcomeBackName": "Welcome back, {name}.",
|
|
1544
|
+
"signIn.oneMomentResume": "One moment while we resume your session.",
|
|
1545
|
+
"signIn.notYouUseDifferent": "Not you? Use a different account",
|
|
1546
|
+
"signIn.chooseWorkspace": "Choose a workspace",
|
|
1547
|
+
"signIn.pickTenantToSignIn": "Pick the tenant to sign in to.",
|
|
1548
|
+
"signIn.subtitleHosted": "Use your work email and password to continue.",
|
|
1549
|
+
"signIn.createAccount": "Create account",
|
|
1550
|
+
"signIn.couldntResume": "Couldn't resume your session.",
|
|
1551
|
+
"signUp.title": "Create your account",
|
|
1552
|
+
"signUp.subtitle": "Start in seconds",
|
|
1553
|
+
"signUp.nameLabel": "Full name",
|
|
1554
|
+
"signUp.namePlaceholder": "Jane Doe",
|
|
1555
|
+
"signUp.emailLabel": "Work email",
|
|
1556
|
+
"signUp.passwordLabel": "Password",
|
|
1557
|
+
"signUp.passwordHint": "At least 8 characters.",
|
|
1558
|
+
"signUp.submit": "Create account",
|
|
1559
|
+
"signUp.submitting": "Creating account\u2026",
|
|
1560
|
+
"signUp.haveAccount": "Already have an account?",
|
|
1561
|
+
"signUp.signIn": "Sign in",
|
|
1562
|
+
"signUp.tenantNameLabel": "Organization name",
|
|
1563
|
+
"signUp.tenantNamePlaceholder": "Acme, Inc.",
|
|
1564
|
+
"signUp.legal": "By creating an account you agree to the Terms of Service and Privacy Policy.",
|
|
1565
|
+
"forgotPassword.title": "Reset your password",
|
|
1566
|
+
"forgotPassword.subtitle": "We'll email you a reset link.",
|
|
1567
|
+
"forgotPassword.submit": "Send reset link",
|
|
1568
|
+
"forgotPassword.submitting": "Sending\u2026",
|
|
1569
|
+
"forgotPassword.sent": "Check your inbox for a reset link.",
|
|
1570
|
+
"forgotPassword.backToSignIn": "Back to sign in",
|
|
1571
|
+
"resetPassword.title": "Choose a new password",
|
|
1572
|
+
"resetPassword.newPasswordLabel": "New password",
|
|
1573
|
+
"resetPassword.confirmPasswordLabel": "Confirm new password",
|
|
1574
|
+
"resetPassword.submit": "Update password",
|
|
1575
|
+
"resetPassword.submitting": "Updating\u2026",
|
|
1576
|
+
"resetPassword.success": "Password updated. You can now sign in.",
|
|
1577
|
+
"resetPassword.mismatch": "Passwords do not match.",
|
|
1578
|
+
"mfa.title": "Verify it's you",
|
|
1579
|
+
"mfa.subtitle": "Enter the verification code to continue.",
|
|
1580
|
+
"mfa.totpLabel": "Authenticator code",
|
|
1581
|
+
"mfa.totpPlaceholder": "123456",
|
|
1582
|
+
"mfa.smsLabel": "Text message code",
|
|
1583
|
+
"mfa.emailLabel": "Email code",
|
|
1584
|
+
"mfa.submit": "Verify",
|
|
1585
|
+
"mfa.submitting": "Verifying\u2026",
|
|
1586
|
+
"mfa.useBackupCode": "Use a backup code",
|
|
1587
|
+
"mfa.useAuthenticator": "Use authenticator app",
|
|
1588
|
+
"mfa.useSms": "Use text message",
|
|
1589
|
+
"mfa.useEmail": "Use email",
|
|
1590
|
+
"mfa.resend": "Resend code",
|
|
1591
|
+
"mfa.resent": "Code sent.",
|
|
1592
|
+
"mfa.backupCodeLabel": "Backup code",
|
|
1593
|
+
"userButton.signedInAs": "Signed in as",
|
|
1594
|
+
"userButton.manageAccount": "Manage account",
|
|
1595
|
+
"userButton.switchOrg": "Switch organization",
|
|
1596
|
+
"userButton.signOut": "Sign out",
|
|
1597
|
+
"userProfile.title": "Your account",
|
|
1598
|
+
"userProfile.profileTab": "Profile",
|
|
1599
|
+
"userProfile.securityTab": "Security",
|
|
1600
|
+
"userProfile.sessionsTab": "Devices & sessions",
|
|
1601
|
+
"userProfile.linkedAccountsTab": "Linked accounts",
|
|
1602
|
+
"userProfile.changeName": "Update name",
|
|
1603
|
+
"userProfile.changeEmail": "Change email",
|
|
1604
|
+
"userProfile.changePassword": "Change password",
|
|
1605
|
+
"userProfile.currentPassword": "Current password",
|
|
1606
|
+
"userProfile.newPassword": "New password",
|
|
1607
|
+
"userProfile.confirmPassword": "Confirm new password",
|
|
1608
|
+
"userProfile.passwordUpdated": "Password updated.",
|
|
1609
|
+
"userProfile.mfaSection": "Two-factor authentication",
|
|
1610
|
+
"userProfile.mfaEnable": "Enable two-factor",
|
|
1611
|
+
"userProfile.mfaDisable": "Disable two-factor",
|
|
1612
|
+
"userProfile.sessionsSection": "Active sessions",
|
|
1613
|
+
"userProfile.revokeSession": "Revoke",
|
|
1614
|
+
"userProfile.revokeAllOthers": "Sign out of all other sessions",
|
|
1615
|
+
"userProfile.thisDevice": "This device",
|
|
1616
|
+
"userProfile.sessionsEmpty": "No active sessions.",
|
|
1617
|
+
"userProfile.linkedAccountsEmpty": "No linked accounts.",
|
|
1618
|
+
"userProfile.connectGoogle": "Connect Google",
|
|
1619
|
+
"userProfile.disconnect": "Disconnect",
|
|
1620
|
+
"orgSwitcher.label": "Organization",
|
|
1621
|
+
"orgSwitcher.personal": "Personal account",
|
|
1622
|
+
"orgSwitcher.createNew": "Create organization",
|
|
1623
|
+
"orgSwitcher.manage": "Manage organization",
|
|
1624
|
+
"orgSwitcher.noOrgs": "You don't belong to any organizations yet.",
|
|
1625
|
+
"orgProfile.title": "Organization settings",
|
|
1626
|
+
"orgProfile.generalTab": "General",
|
|
1627
|
+
"orgProfile.membersTab": "Members",
|
|
1628
|
+
"orgProfile.invitationsTab": "Invitations",
|
|
1629
|
+
"orgProfile.dangerTab": "Danger zone",
|
|
1630
|
+
"orgProfile.invite": "Invite member",
|
|
1631
|
+
"orgProfile.inviteEmailLabel": "Email address",
|
|
1632
|
+
"orgProfile.inviteRoleLabel": "Role",
|
|
1633
|
+
"orgProfile.inviteSend": "Send invitation",
|
|
1634
|
+
"orgProfile.inviteSent": "Invitation sent.",
|
|
1635
|
+
"orgProfile.removeMember": "Remove",
|
|
1636
|
+
"orgProfile.deleteOrg": "Permanently delete organization",
|
|
1637
|
+
"orgProfile.deleteOrgConfirm": "Type the organization name to confirm.",
|
|
1638
|
+
"createOrg.title": "Create an organization",
|
|
1639
|
+
"createOrg.nameLabel": "Organization name",
|
|
1640
|
+
"createOrg.submit": "Create",
|
|
1641
|
+
"createOrg.submitting": "Creating\u2026",
|
|
1642
|
+
"waitlist.title": "Join the waitlist",
|
|
1643
|
+
"waitlist.subtitle": "We'll notify you when access opens up.",
|
|
1644
|
+
"waitlist.submit": "Join waitlist",
|
|
1645
|
+
"waitlist.submitting": "Joining\u2026",
|
|
1646
|
+
"waitlist.success": "You're on the list. We'll be in touch.",
|
|
1647
|
+
"impersonation.banner": "Impersonation active \u2014 you are signed in as {targetEmail}.",
|
|
1648
|
+
"impersonation.exit": "Exit impersonation",
|
|
1649
|
+
"magicLink.title": "Check your email",
|
|
1650
|
+
"magicLink.subtitle": "We sent a sign-in link to {email}.",
|
|
1651
|
+
"magicLink.resend": "Resend link",
|
|
1652
|
+
"magicLink.changeEmail": "Use a different email",
|
|
1653
|
+
"errors.generic": "Something went wrong. Please try again.",
|
|
1654
|
+
"errors.network": "Network error. Check your connection and try again.",
|
|
1655
|
+
"errors.invalidCredentials": "Incorrect email or password.",
|
|
1656
|
+
"errors.userNotFound": "No account found for that email.",
|
|
1657
|
+
"errors.emailInUse": "An account already exists for that email.",
|
|
1658
|
+
"errors.weakPassword": "Password is too weak.",
|
|
1659
|
+
"errors.mfaInvalid": "That code didn't work. Try again.",
|
|
1660
|
+
"errors.mfaExpired": "That code has expired. Request a new one.",
|
|
1661
|
+
"errors.tooManyAttempts": "Too many attempts. Please wait and try again.",
|
|
1662
|
+
"errors.sessionExpired": "Your session expired. Please sign in again.",
|
|
1663
|
+
"errors.permissionDenied": "You don't have permission to do that.",
|
|
1664
|
+
"errors.notFound": "Not found.",
|
|
1665
|
+
"errors.serverError": "Server error. Please try again shortly.",
|
|
1666
|
+
"errors.invalidEmail": "Enter a valid email address.",
|
|
1667
|
+
"errors.passwordTooShort": "Password must be at least {min} characters.",
|
|
1668
|
+
"errors.required": "This field is required.",
|
|
1669
|
+
"errors.invitationInvalid": "This invitation link is invalid or expired.",
|
|
1670
|
+
"validation.emailRequired": "Email is required.",
|
|
1671
|
+
"validation.emailInvalid": "Enter a valid email address.",
|
|
1672
|
+
"validation.passwordRequired": "Password is required.",
|
|
1673
|
+
"validation.nameRequired": "Name is required.",
|
|
1674
|
+
"validation.codeRequired": "Verification code is required.",
|
|
1675
|
+
"validation.codeInvalid": "Code must be 6 digits."
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
// src/locales/index.ts
|
|
1679
|
+
var defaultBundle = enUS;
|
|
1680
|
+
function t(bundle, key, vars) {
|
|
1681
|
+
const fromBundle = bundle && bundle[key];
|
|
1682
|
+
const raw = typeof fromBundle === "string" && fromBundle || defaultBundle[key];
|
|
1683
|
+
if (!vars) return raw;
|
|
1684
|
+
return raw.replace(/\{(\w+)\}/g, (_match, name) => {
|
|
1685
|
+
const v = vars[name];
|
|
1686
|
+
return v === void 0 || v === null ? `{${name}}` : String(v);
|
|
1687
|
+
});
|
|
635
1688
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
return base64UrlEncode(new Uint8Array(digest));
|
|
1689
|
+
function resolveBundle(override) {
|
|
1690
|
+
if (!override) return defaultBundle;
|
|
1691
|
+
return { ...defaultBundle, ...override };
|
|
640
1692
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1693
|
+
var ERROR_CODE_MAP = {
|
|
1694
|
+
INVALID_CREDENTIALS: "errors.invalidCredentials",
|
|
1695
|
+
USER_NOT_FOUND: "errors.userNotFound",
|
|
1696
|
+
EMAIL_IN_USE: "errors.emailInUse",
|
|
1697
|
+
EMAIL_ALREADY_EXISTS: "errors.emailInUse",
|
|
1698
|
+
WEAK_PASSWORD: "errors.weakPassword",
|
|
1699
|
+
MFA_INVALID: "errors.mfaInvalid",
|
|
1700
|
+
MFA_CODE_INVALID: "errors.mfaInvalid",
|
|
1701
|
+
MFA_EXPIRED: "errors.mfaExpired",
|
|
1702
|
+
MFA_CODE_EXPIRED: "errors.mfaExpired",
|
|
1703
|
+
RATE_LIMITED: "errors.tooManyAttempts",
|
|
1704
|
+
TOO_MANY_REQUESTS: "errors.tooManyAttempts",
|
|
1705
|
+
SESSION_EXPIRED: "errors.sessionExpired",
|
|
1706
|
+
TOKEN_EXPIRED: "errors.sessionExpired",
|
|
1707
|
+
FORBIDDEN: "errors.permissionDenied",
|
|
1708
|
+
PERMISSION_DENIED: "errors.permissionDenied",
|
|
1709
|
+
NOT_FOUND: "errors.notFound",
|
|
1710
|
+
INTERNAL_ERROR: "errors.serverError",
|
|
1711
|
+
SERVER_ERROR: "errors.serverError",
|
|
1712
|
+
NETWORK_ERROR: "errors.network",
|
|
1713
|
+
INVITATION_INVALID: "errors.invitationInvalid",
|
|
1714
|
+
INVITATION_EXPIRED: "errors.invitationInvalid"
|
|
1715
|
+
};
|
|
1716
|
+
function localizeErrorCode(bundle, code, vars) {
|
|
1717
|
+
if (!code) return t(bundle, "errors.generic", vars);
|
|
1718
|
+
const key = ERROR_CODE_MAP[code.toUpperCase()];
|
|
1719
|
+
return key ? t(bundle, key, vars) : t(bundle, "errors.generic", vars);
|
|
647
1720
|
}
|
|
648
1721
|
|
|
649
|
-
// src/browser/
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
function defaultRedirectUri() {
|
|
656
|
-
if (typeof window === "undefined") {
|
|
657
|
-
throw new Error("redirectToSignIn requires a browser environment (window)");
|
|
1722
|
+
// src/browser/returnTo.ts
|
|
1723
|
+
function normalizeOrigin(o) {
|
|
1724
|
+
try {
|
|
1725
|
+
return new URL(o).origin;
|
|
1726
|
+
} catch {
|
|
1727
|
+
return o.replace(/\/+$/, "");
|
|
658
1728
|
}
|
|
659
|
-
return `${window.location.origin}${DEFAULT_CALLBACK_PATH}`;
|
|
660
1729
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
663
|
-
return
|
|
1730
|
+
function sanitizeReturnTo(input, options = {}) {
|
|
1731
|
+
const fallback = options.fallback ?? "/";
|
|
1732
|
+
if (!input || typeof input !== "string") return fallback;
|
|
1733
|
+
const trimmed = input.trim();
|
|
1734
|
+
if (!trimmed) return fallback;
|
|
1735
|
+
if (trimmed.startsWith("//")) return fallback;
|
|
1736
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
1737
|
+
return trimmed;
|
|
1738
|
+
}
|
|
1739
|
+
if (!/^[a-z][a-z0-9+\-.]*:/i.test(trimmed)) {
|
|
1740
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
1741
|
+
}
|
|
1742
|
+
let parsed;
|
|
1743
|
+
try {
|
|
1744
|
+
parsed = new URL(trimmed);
|
|
1745
|
+
} catch {
|
|
1746
|
+
return fallback;
|
|
1747
|
+
}
|
|
1748
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return fallback;
|
|
1749
|
+
const currentOrigin = options.currentOrigin ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
1750
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
1751
|
+
if (currentOrigin) allowed.add(normalizeOrigin(currentOrigin));
|
|
1752
|
+
for (const o of options.allowedOrigins ?? []) allowed.add(normalizeOrigin(o));
|
|
1753
|
+
if (allowed.has(parsed.origin)) return parsed.toString();
|
|
1754
|
+
return fallback;
|
|
664
1755
|
}
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
codeVerifier: pkce.codeVerifier,
|
|
671
|
-
state: pkce.state,
|
|
672
|
-
nonce: pkce.nonce,
|
|
673
|
-
redirectUri,
|
|
674
|
-
appKey: manager.publishableKey.raw,
|
|
675
|
-
returnTo,
|
|
676
|
-
createdAt: Date.now()
|
|
677
|
-
});
|
|
678
|
-
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
|
|
679
|
-
url.searchParams.set("response_type", "code");
|
|
680
|
-
url.searchParams.set("app", manager.appKey);
|
|
681
|
-
url.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
682
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
683
|
-
url.searchParams.set("state", pkce.state);
|
|
684
|
-
url.searchParams.set("nonce", pkce.nonce);
|
|
685
|
-
url.searchParams.set("code_challenge", pkce.codeChallenge);
|
|
686
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
687
|
-
url.searchParams.set("scope", opts.scope ?? "openid profile email");
|
|
688
|
-
url.searchParams.set("return_to", returnTo);
|
|
689
|
-
return url.toString();
|
|
1756
|
+
function isReturnToAllowed(input, options = {}) {
|
|
1757
|
+
const fallback = options.fallback ?? "/";
|
|
1758
|
+
const out = sanitizeReturnTo(input, options);
|
|
1759
|
+
if (!input) return false;
|
|
1760
|
+
return out !== fallback || input === fallback;
|
|
690
1761
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
window.location.assign(url);
|
|
1762
|
+
|
|
1763
|
+
// src/browser/passwordless.ts
|
|
1764
|
+
function url(base, path) {
|
|
1765
|
+
return `${base.replace(/\/+$/, "")}${path}`;
|
|
697
1766
|
}
|
|
698
|
-
|
|
699
|
-
|
|
1767
|
+
function headers(o) {
|
|
1768
|
+
const h = { "Content-Type": "application/json" };
|
|
1769
|
+
if (o.cookieSession !== false) h["x-iqauth-session"] = "cookie";
|
|
1770
|
+
return h;
|
|
700
1771
|
}
|
|
701
|
-
async function
|
|
702
|
-
const
|
|
703
|
-
const code = url.searchParams.get("code");
|
|
704
|
-
const state = url.searchParams.get("state");
|
|
705
|
-
const errorParam = url.searchParams.get("error");
|
|
706
|
-
if (errorParam) {
|
|
707
|
-
return { ok: false, returnTo: "/", error: errorParam };
|
|
708
|
-
}
|
|
709
|
-
if (!code || !state) {
|
|
710
|
-
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
711
|
-
}
|
|
712
|
-
const record = loadPkce(state);
|
|
713
|
-
if (!record) {
|
|
714
|
-
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
715
|
-
}
|
|
716
|
-
clearPkce(state);
|
|
717
|
-
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
718
|
-
if (!fetchImpl) {
|
|
719
|
-
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
720
|
-
}
|
|
721
|
-
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
722
|
-
const res = await fetchImpl(tokenUrl, {
|
|
1772
|
+
async function postJson(u, body, h) {
|
|
1773
|
+
const r = await fetch(u, {
|
|
723
1774
|
method: "POST",
|
|
724
1775
|
credentials: "include",
|
|
725
|
-
headers:
|
|
726
|
-
body: JSON.stringify(
|
|
727
|
-
grant_type: "authorization_code",
|
|
728
|
-
code,
|
|
729
|
-
redirect_uri: record.redirectUri,
|
|
730
|
-
client_id: manager.appKey,
|
|
731
|
-
code_verifier: record.codeVerifier
|
|
732
|
-
})
|
|
1776
|
+
headers: h,
|
|
1777
|
+
body: JSON.stringify(body)
|
|
733
1778
|
});
|
|
734
|
-
const
|
|
735
|
-
if (!
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
const tokens = body;
|
|
740
|
-
if (!tokens.access_token) {
|
|
741
|
-
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
742
|
-
}
|
|
743
|
-
if (tokens.refresh_token) {
|
|
744
|
-
setCookie(REFRESH_COOKIE, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
1779
|
+
const p = await r.json().catch(() => ({}));
|
|
1780
|
+
if (!r.ok) {
|
|
1781
|
+
const msg = p?.error?.message || `Request failed (${r.status})`;
|
|
1782
|
+
throw new Error(msg);
|
|
745
1783
|
}
|
|
746
|
-
|
|
747
|
-
return { ok: true, returnTo: record.returnTo };
|
|
1784
|
+
return p.data;
|
|
748
1785
|
}
|
|
749
|
-
async function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1786
|
+
async function requestMagicLink(opts, input) {
|
|
1787
|
+
await postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/magic-link/request"), input, headers(opts));
|
|
1788
|
+
return { ok: true };
|
|
1789
|
+
}
|
|
1790
|
+
async function beginPasskeyAuthentication(opts, input = {}) {
|
|
1791
|
+
return postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/passkeys/authentication/options"), input, headers(opts));
|
|
1792
|
+
}
|
|
1793
|
+
async function finishPasskeyAuthentication(opts, response) {
|
|
1794
|
+
return postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/passkeys/authentication/verify"), { response }, headers(opts));
|
|
1795
|
+
}
|
|
1796
|
+
async function beginPasskeyRegistration(opts) {
|
|
1797
|
+
return postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/passkeys/registration/options"), {}, headers(opts));
|
|
1798
|
+
}
|
|
1799
|
+
async function finishPasskeyRegistration(opts, response, name) {
|
|
1800
|
+
return postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/passkeys/registration/verify"), { response, name }, headers(opts));
|
|
1801
|
+
}
|
|
1802
|
+
async function signInWithPasskey(opts, input = {}) {
|
|
1803
|
+
const mod = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
1804
|
+
const options = await beginPasskeyAuthentication(opts, input);
|
|
1805
|
+
const response = await mod.startAuthentication({ optionsJSON: options });
|
|
1806
|
+
return finishPasskeyAuthentication(opts, response);
|
|
1807
|
+
}
|
|
1808
|
+
async function enrollPasskey(opts, name) {
|
|
1809
|
+
const mod = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
1810
|
+
const options = await beginPasskeyRegistration(opts);
|
|
1811
|
+
const response = await mod.startRegistration({ optionsJSON: options });
|
|
1812
|
+
return finishPasskeyRegistration(opts, response, name);
|
|
1813
|
+
}
|
|
1814
|
+
async function listLinkedIdentities(opts) {
|
|
1815
|
+
const r = await fetch(url(opts.iqAuthBaseUrl, "/api/v1/auth/identities"), { credentials: "include" });
|
|
1816
|
+
const p = await r.json().catch(() => ({}));
|
|
1817
|
+
if (!r.ok) throw new Error(p?.error?.message || `Request failed (${r.status})`);
|
|
1818
|
+
return p?.data?.identities ?? [];
|
|
1819
|
+
}
|
|
1820
|
+
async function linkProvider(opts, input) {
|
|
1821
|
+
await postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/identities/link"), input, headers(opts));
|
|
1822
|
+
return { ok: true };
|
|
1823
|
+
}
|
|
1824
|
+
async function unlinkProvider(opts, input) {
|
|
1825
|
+
await postJson(url(opts.iqAuthBaseUrl, "/api/v1/auth/identities/unlink"), input, headers(opts));
|
|
1826
|
+
return { ok: true };
|
|
772
1827
|
}
|
|
773
1828
|
|
|
774
1829
|
// src/react/index.tsx
|
|
775
1830
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
1831
|
+
var globalManager = null;
|
|
1832
|
+
function setGlobalManager(m) {
|
|
1833
|
+
globalManager = m;
|
|
1834
|
+
}
|
|
1835
|
+
function getGlobalManager() {
|
|
1836
|
+
return globalManager;
|
|
1837
|
+
}
|
|
776
1838
|
var IQAuthContext = (0, import_react.createContext)(null);
|
|
777
1839
|
function IQAuthProvider({
|
|
778
1840
|
publishableKey,
|
|
@@ -780,6 +1842,11 @@ function IQAuthProvider({
|
|
|
780
1842
|
channelName,
|
|
781
1843
|
proactiveRefresh,
|
|
782
1844
|
manager: externalManager,
|
|
1845
|
+
allowedReturnOrigins,
|
|
1846
|
+
appearance,
|
|
1847
|
+
roleMapper,
|
|
1848
|
+
cookieNames,
|
|
1849
|
+
localization,
|
|
783
1850
|
children
|
|
784
1851
|
}) {
|
|
785
1852
|
const managerRef = (0, import_react.useRef)(null);
|
|
@@ -788,8 +1855,10 @@ function IQAuthProvider({
|
|
|
788
1855
|
publishableKey,
|
|
789
1856
|
issuer,
|
|
790
1857
|
channelName,
|
|
791
|
-
proactiveRefresh
|
|
1858
|
+
proactiveRefresh,
|
|
1859
|
+
cookieNames
|
|
792
1860
|
});
|
|
1861
|
+
setGlobalManager(managerRef.current);
|
|
793
1862
|
}
|
|
794
1863
|
const manager = managerRef.current;
|
|
795
1864
|
const subscribe = (0, import_react.useCallback)(
|
|
@@ -815,7 +1884,21 @@ function IQAuthProvider({
|
|
|
815
1884
|
}
|
|
816
1885
|
};
|
|
817
1886
|
}, [manager]);
|
|
818
|
-
const
|
|
1887
|
+
const resolvedLocalization = (0, import_react.useMemo)(
|
|
1888
|
+
() => resolveBundle(localization),
|
|
1889
|
+
[localization]
|
|
1890
|
+
);
|
|
1891
|
+
const value = (0, import_react.useMemo)(
|
|
1892
|
+
() => ({
|
|
1893
|
+
manager,
|
|
1894
|
+
snapshot,
|
|
1895
|
+
allowedReturnOrigins: allowedReturnOrigins ?? [],
|
|
1896
|
+
appearance: appearance ?? null,
|
|
1897
|
+
roleMapper: roleMapper ?? null,
|
|
1898
|
+
localization: resolvedLocalization
|
|
1899
|
+
}),
|
|
1900
|
+
[manager, snapshot, allowedReturnOrigins, appearance, roleMapper, resolvedLocalization]
|
|
1901
|
+
);
|
|
819
1902
|
return (0, import_react.createElement)(IQAuthContext.Provider, { value }, children);
|
|
820
1903
|
}
|
|
821
1904
|
function useCtx() {
|
|
@@ -823,16 +1906,56 @@ function useCtx() {
|
|
|
823
1906
|
if (!ctx) throw new Error("IQAuth hooks must be used inside <IQAuthProvider>");
|
|
824
1907
|
return ctx;
|
|
825
1908
|
}
|
|
1909
|
+
function useLocale() {
|
|
1910
|
+
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
1911
|
+
return ctx?.localization ?? defaultBundle;
|
|
1912
|
+
}
|
|
1913
|
+
function useT() {
|
|
1914
|
+
const bundle = useLocale();
|
|
1915
|
+
return (0, import_react.useMemo)(
|
|
1916
|
+
() => (key, vars) => t(bundle, key, vars),
|
|
1917
|
+
[bundle]
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
function localizeError(bundle, raw) {
|
|
1921
|
+
if (!raw) return t(bundle, "errors.generic");
|
|
1922
|
+
if (typeof raw === "string") {
|
|
1923
|
+
if (/^[A-Z][A-Z0-9_]+$/.test(raw)) return localizeErrorCode(bundle, raw);
|
|
1924
|
+
return raw;
|
|
1925
|
+
}
|
|
1926
|
+
if (raw.code) return localizeErrorCode(bundle, raw.code);
|
|
1927
|
+
return raw.message || t(bundle, "errors.generic");
|
|
1928
|
+
}
|
|
826
1929
|
function useUser() {
|
|
827
|
-
const { snapshot } = useCtx();
|
|
1930
|
+
const { snapshot, roleMapper } = useCtx();
|
|
828
1931
|
return (0, import_react.useMemo)(
|
|
829
|
-
() =>
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1932
|
+
() => {
|
|
1933
|
+
const user = snapshot.user ? {
|
|
1934
|
+
...snapshot.user,
|
|
1935
|
+
// F13 — derive `role` via roleMapper, falling back to a sensible default
|
|
1936
|
+
role: (() => {
|
|
1937
|
+
if (roleMapper) {
|
|
1938
|
+
try {
|
|
1939
|
+
return roleMapper(snapshot.claims);
|
|
1940
|
+
} catch {
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const c = snapshot.claims;
|
|
1945
|
+
if (!c) return null;
|
|
1946
|
+
if (typeof c.role === "string" && c.role) return c.role;
|
|
1947
|
+
if (Array.isArray(c.roles) && c.roles.length > 0 && typeof c.roles[0] === "string") return c.roles[0];
|
|
1948
|
+
return null;
|
|
1949
|
+
})()
|
|
1950
|
+
} : null;
|
|
1951
|
+
return {
|
|
1952
|
+
isLoaded: snapshot.status !== "loading",
|
|
1953
|
+
isSignedIn: snapshot.status === "authenticated" && !!snapshot.user,
|
|
1954
|
+
user,
|
|
1955
|
+
error: snapshot.error
|
|
1956
|
+
};
|
|
1957
|
+
},
|
|
1958
|
+
[snapshot.status, snapshot.user, snapshot.claims, snapshot.error, snapshot.version, roleMapper]
|
|
836
1959
|
);
|
|
837
1960
|
}
|
|
838
1961
|
function useSession() {
|
|
@@ -884,6 +2007,148 @@ function useAuthFetch() {
|
|
|
884
2007
|
[manager]
|
|
885
2008
|
);
|
|
886
2009
|
}
|
|
2010
|
+
function useSessionList() {
|
|
2011
|
+
const { manager } = useCtx();
|
|
2012
|
+
const [sessions, setSessions] = (0, import_react.useState)([]);
|
|
2013
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
2014
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
2015
|
+
const base = manager.issuerUrl.replace(/\/$/, "");
|
|
2016
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
2017
|
+
setLoading(true);
|
|
2018
|
+
setError(null);
|
|
2019
|
+
try {
|
|
2020
|
+
const res = await manager.fetch(`${base}/api/v1/users/me/sessions`);
|
|
2021
|
+
const json = await res.json().catch(() => ({}));
|
|
2022
|
+
if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
|
|
2023
|
+
setSessions(json?.data?.sessions || []);
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
setError(err.message);
|
|
2026
|
+
} finally {
|
|
2027
|
+
setLoading(false);
|
|
2028
|
+
}
|
|
2029
|
+
}, [manager, base]);
|
|
2030
|
+
(0, import_react.useEffect)(() => {
|
|
2031
|
+
void refresh();
|
|
2032
|
+
}, [refresh]);
|
|
2033
|
+
const revoke = (0, import_react.useCallback)(async (sessionId) => {
|
|
2034
|
+
const res = await manager.fetch(`${base}/api/v1/users/me/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
2035
|
+
if (!res.ok) {
|
|
2036
|
+
const j = await res.json().catch(() => ({}));
|
|
2037
|
+
throw new Error(j?.error?.message || `HTTP ${res.status}`);
|
|
2038
|
+
}
|
|
2039
|
+
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
|
2040
|
+
}, [manager, base]);
|
|
2041
|
+
const revokeAllOthers = (0, import_react.useCallback)(async () => {
|
|
2042
|
+
const res = await manager.fetch(`${base}/api/v1/users/me/sessions/revoke-all-others`, { method: "POST" });
|
|
2043
|
+
const json = await res.json().catch(() => ({}));
|
|
2044
|
+
if (!res.ok) throw new Error(json?.error?.message || `HTTP ${res.status}`);
|
|
2045
|
+
setSessions((prev) => prev.filter((s) => s.isCurrent));
|
|
2046
|
+
return { terminatedCount: json?.data?.terminatedCount ?? 0 };
|
|
2047
|
+
}, [manager, base]);
|
|
2048
|
+
return { sessions, loading, error, refresh, revoke, revokeAllOthers };
|
|
2049
|
+
}
|
|
2050
|
+
async function revokeSession(sessionId) {
|
|
2051
|
+
const mgr = getGlobalManager();
|
|
2052
|
+
if (!mgr) throw new Error("revokeSession() requires <IQAuthProvider> mounted");
|
|
2053
|
+
const base = mgr.issuerUrl.replace(/\/$/, "");
|
|
2054
|
+
const res = await mgr.fetch(`${base}/api/v1/users/me/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
|
2055
|
+
if (!res.ok) {
|
|
2056
|
+
const j = await res.json().catch(() => ({}));
|
|
2057
|
+
throw new Error(j?.error?.message || `HTTP ${res.status}`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
var MultisessionContext = (0, import_react.createContext)(null);
|
|
2061
|
+
function MultisessionAppSupport({ children }) {
|
|
2062
|
+
const { manager, snapshot } = useCtx();
|
|
2063
|
+
const registryRef = (0, import_react.useRef)(null);
|
|
2064
|
+
if (!registryRef.current) {
|
|
2065
|
+
registryRef.current = new AccountRegistry(manager.appKey);
|
|
2066
|
+
}
|
|
2067
|
+
const registry = registryRef.current;
|
|
2068
|
+
const [version, setVersion] = (0, import_react.useState)(0);
|
|
2069
|
+
(0, import_react.useEffect)(() => {
|
|
2070
|
+
const store = new MultiAccountTokenStore(registry, defaultCookieStore());
|
|
2071
|
+
manager.setTokenStore(store);
|
|
2072
|
+
const unsub = registry.subscribe(() => setVersion((v) => v + 1));
|
|
2073
|
+
return () => {
|
|
2074
|
+
unsub();
|
|
2075
|
+
registry.destroy();
|
|
2076
|
+
};
|
|
2077
|
+
}, [manager]);
|
|
2078
|
+
(0, import_react.useEffect)(() => {
|
|
2079
|
+
if (snapshot.status !== "authenticated" || !snapshot.user) return;
|
|
2080
|
+
const claims = snapshot.claims;
|
|
2081
|
+
const rec = {
|
|
2082
|
+
accountId: snapshot.user.sub,
|
|
2083
|
+
userId: snapshot.user.sub,
|
|
2084
|
+
email: snapshot.user.email,
|
|
2085
|
+
name: snapshot.user.name,
|
|
2086
|
+
tenantId: snapshot.tenantId ?? claims?.tenantId ?? null,
|
|
2087
|
+
addedAt: Date.now()
|
|
2088
|
+
};
|
|
2089
|
+
registry.upsert(rec);
|
|
2090
|
+
if (registry.active() !== rec.accountId) {
|
|
2091
|
+
registry.setActive(rec.accountId);
|
|
2092
|
+
}
|
|
2093
|
+
}, [snapshot.user?.sub, snapshot.tenantId, snapshot.status]);
|
|
2094
|
+
const value = (0, import_react.useMemo)(
|
|
2095
|
+
() => ({ registry, version }),
|
|
2096
|
+
[registry, version]
|
|
2097
|
+
);
|
|
2098
|
+
return (0, import_react.createElement)(MultisessionContext.Provider, { value }, children);
|
|
2099
|
+
}
|
|
2100
|
+
function useMultiCtx() {
|
|
2101
|
+
const ctx = (0, import_react.useContext)(MultisessionContext);
|
|
2102
|
+
if (!ctx) throw new Error("F22 hooks must be used inside <MultisessionAppSupport>");
|
|
2103
|
+
return ctx;
|
|
2104
|
+
}
|
|
2105
|
+
function useAccountList() {
|
|
2106
|
+
const { registry, version } = useMultiCtx();
|
|
2107
|
+
const { isLoaded } = useUser();
|
|
2108
|
+
const accounts = (0, import_react.useMemo)(() => {
|
|
2109
|
+
const active = registry.active();
|
|
2110
|
+
return registry.list().map((a) => ({
|
|
2111
|
+
accountId: a.accountId,
|
|
2112
|
+
userId: a.userId,
|
|
2113
|
+
email: a.email,
|
|
2114
|
+
name: a.name,
|
|
2115
|
+
tenantId: a.tenantId,
|
|
2116
|
+
isActive: a.accountId === active
|
|
2117
|
+
}));
|
|
2118
|
+
}, [registry, version]);
|
|
2119
|
+
return { accounts, loading: !isLoaded };
|
|
2120
|
+
}
|
|
2121
|
+
function useAccountSwitcher() {
|
|
2122
|
+
const { manager } = useCtx();
|
|
2123
|
+
const { registry } = useMultiCtx();
|
|
2124
|
+
const addAccount = (0, import_react.useCallback)(
|
|
2125
|
+
async (opts = {}) => {
|
|
2126
|
+
registry.setActive(null);
|
|
2127
|
+
await signIn(manager, { ...opts, prompt: "login" });
|
|
2128
|
+
},
|
|
2129
|
+
[manager, registry]
|
|
2130
|
+
);
|
|
2131
|
+
const switchTo = (0, import_react.useCallback)(
|
|
2132
|
+
async (accountId) => {
|
|
2133
|
+
const target = registry.get(accountId);
|
|
2134
|
+
if (!target) throw new Error(`Unknown accountId: ${accountId}`);
|
|
2135
|
+
registry.setActive(accountId);
|
|
2136
|
+
const ok = await manager.refresh();
|
|
2137
|
+
if (!ok) {
|
|
2138
|
+
registry.remove(accountId);
|
|
2139
|
+
throw new Error("Could not switch account; session may have been revoked");
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2142
|
+
[manager, registry]
|
|
2143
|
+
);
|
|
2144
|
+
const removeAccount = (0, import_react.useCallback)(
|
|
2145
|
+
(accountId) => {
|
|
2146
|
+
registry.remove(accountId);
|
|
2147
|
+
},
|
|
2148
|
+
[registry]
|
|
2149
|
+
);
|
|
2150
|
+
return { addAccount, switchTo, removeAccount };
|
|
2151
|
+
}
|
|
887
2152
|
function SignedIn({ children }) {
|
|
888
2153
|
const { isSignedIn, isLoaded } = useUser();
|
|
889
2154
|
if (!isLoaded || !isSignedIn) return null;
|
|
@@ -947,6 +2212,113 @@ function RedirectToSignIn(props = {}) {
|
|
|
947
2212
|
}
|
|
948
2213
|
return null;
|
|
949
2214
|
}
|
|
2215
|
+
function asArray(v) {
|
|
2216
|
+
if (v == null) return [];
|
|
2217
|
+
return Array.isArray(v) ? v : [v];
|
|
2218
|
+
}
|
|
2219
|
+
function claimRoles(c) {
|
|
2220
|
+
if (!c) return [];
|
|
2221
|
+
const x = c;
|
|
2222
|
+
if (Array.isArray(x.roles)) return x.roles.filter((r) => typeof r === "string");
|
|
2223
|
+
if (typeof x.role === "string") return [x.role];
|
|
2224
|
+
return [];
|
|
2225
|
+
}
|
|
2226
|
+
function claimPermissions(c) {
|
|
2227
|
+
if (!c) return [];
|
|
2228
|
+
const x = c;
|
|
2229
|
+
const out = /* @__PURE__ */ new Set();
|
|
2230
|
+
if (Array.isArray(x.permissions)) {
|
|
2231
|
+
for (const p of x.permissions) if (typeof p === "string") out.add(p);
|
|
2232
|
+
}
|
|
2233
|
+
if (Array.isArray(x.entitlements)) {
|
|
2234
|
+
for (const p of x.entitlements) if (typeof p === "string") out.add(p);
|
|
2235
|
+
}
|
|
2236
|
+
if (typeof x.scope === "string") {
|
|
2237
|
+
for (const s of x.scope.split(/\s+/)) if (s) out.add(s);
|
|
2238
|
+
}
|
|
2239
|
+
return Array.from(out);
|
|
2240
|
+
}
|
|
2241
|
+
function Protect({ role, permission, condition, fallback = null, children }) {
|
|
2242
|
+
const { snapshot } = useCtx();
|
|
2243
|
+
if (snapshot.status !== "authenticated") return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2244
|
+
const wantedRoles = asArray(role);
|
|
2245
|
+
const wantedPerms = asArray(permission);
|
|
2246
|
+
if (wantedRoles.length) {
|
|
2247
|
+
const have = new Set(claimRoles(snapshot.claims));
|
|
2248
|
+
if (!wantedRoles.some((r) => have.has(r))) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2249
|
+
}
|
|
2250
|
+
if (wantedPerms.length) {
|
|
2251
|
+
const have = new Set(claimPermissions(snapshot.claims));
|
|
2252
|
+
if (!wantedPerms.some((p) => have.has(p))) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2253
|
+
}
|
|
2254
|
+
if (condition && !condition(snapshot.claims)) return (0, import_react.createElement)(import_react.Fragment, null, fallback);
|
|
2255
|
+
return (0, import_react.createElement)(import_react.Fragment, null, children);
|
|
2256
|
+
}
|
|
2257
|
+
function RedirectToSignedIn({ to = "/", replace = true } = {}) {
|
|
2258
|
+
const { snapshot, allowedReturnOrigins } = useCtx();
|
|
2259
|
+
(0, import_react.useEffect)(() => {
|
|
2260
|
+
if (snapshot.status !== "authenticated") return;
|
|
2261
|
+
if (typeof window === "undefined") return;
|
|
2262
|
+
const safe = sanitizeReturnTo(to, { allowedOrigins: allowedReturnOrigins, fallback: "/" });
|
|
2263
|
+
if (replace) window.location.replace(safe);
|
|
2264
|
+
else window.location.assign(safe);
|
|
2265
|
+
}, [snapshot.status, to, replace, allowedReturnOrigins]);
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
function useReturnTo(options = {}) {
|
|
2269
|
+
const { allowedReturnOrigins } = useCtx();
|
|
2270
|
+
const paramName = options.paramName ?? "return_to";
|
|
2271
|
+
const storageKey2 = options.storageKey ?? "iqauth_return_to";
|
|
2272
|
+
const fallback = options.fallback ?? "/";
|
|
2273
|
+
return (0, import_react.useMemo)(() => {
|
|
2274
|
+
if (typeof window === "undefined") return fallback;
|
|
2275
|
+
let raw = null;
|
|
2276
|
+
try {
|
|
2277
|
+
const params = new URLSearchParams(window.location.search);
|
|
2278
|
+
raw = params.get(paramName) || params.get("next");
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
if (!raw) {
|
|
2282
|
+
try {
|
|
2283
|
+
raw = window.sessionStorage.getItem(storageKey2);
|
|
2284
|
+
} catch {
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
const safe = sanitizeReturnTo(raw, { allowedOrigins: allowedReturnOrigins, fallback });
|
|
2288
|
+
if (safe !== fallback) {
|
|
2289
|
+
try {
|
|
2290
|
+
window.sessionStorage.setItem(storageKey2, safe);
|
|
2291
|
+
} catch {
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
return safe;
|
|
2295
|
+
}, [paramName, storageKey2, fallback, allowedReturnOrigins]);
|
|
2296
|
+
}
|
|
2297
|
+
function IQAuthReturnToBouncer({ children, ...opts }) {
|
|
2298
|
+
const { snapshot } = useCtx();
|
|
2299
|
+
const returnTo = useReturnTo(opts);
|
|
2300
|
+
(0, import_react.useEffect)(() => {
|
|
2301
|
+
if (snapshot.status !== "authenticated") return;
|
|
2302
|
+
if (typeof window === "undefined") return;
|
|
2303
|
+
window.location.replace(returnTo);
|
|
2304
|
+
}, [snapshot.status, returnTo]);
|
|
2305
|
+
if (snapshot.status === "authenticated") return null;
|
|
2306
|
+
return (0, import_react.createElement)(import_react.Fragment, null, children);
|
|
2307
|
+
}
|
|
2308
|
+
async function preflightReturnTo(args) {
|
|
2309
|
+
const f = args.fetchImpl ?? fetch;
|
|
2310
|
+
const url2 = `${args.iqAuthBaseUrl.replace(/\/$/, "")}/api/public/apps/${encodeURIComponent(args.appKey)}/sign-in-context?return_to=${encodeURIComponent(args.returnTo)}`;
|
|
2311
|
+
try {
|
|
2312
|
+
const r = await f(url2, { credentials: "include" });
|
|
2313
|
+
const body = await r.json().catch(() => null);
|
|
2314
|
+
if (!r.ok || !body?.success || !body.data) {
|
|
2315
|
+
return { ok: false, allowedOrigins: [], reason: body?.error?.message || `HTTP ${r.status}` };
|
|
2316
|
+
}
|
|
2317
|
+
return { ok: !!body.data.returnAllowed, allowedOrigins: body.data.allowedOrigins ?? [], reason: body.data.returnAllowed ? void 0 : "returnTo not in app allowedOrigins" };
|
|
2318
|
+
} catch (err) {
|
|
2319
|
+
return { ok: false, allowedOrigins: [], reason: err.message };
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
950
2322
|
function AuthCallback({ onComplete, fallback } = {}) {
|
|
951
2323
|
const { manager } = useCtx();
|
|
952
2324
|
(0, import_react.useEffect)(() => {
|
|
@@ -980,8 +2352,8 @@ function brandStyle(branding) {
|
|
|
980
2352
|
if (branding.fontFamilyHeading) s["--brand-font-heading"] = branding.fontFamilyHeading;
|
|
981
2353
|
return s;
|
|
982
2354
|
}
|
|
983
|
-
async function jsonFetch(
|
|
984
|
-
const res = await fetch(
|
|
2355
|
+
async function jsonFetch(url2, init) {
|
|
2356
|
+
const res = await fetch(url2, { ...init, credentials: init?.credentials || "include" });
|
|
985
2357
|
const payload = await res.json().catch(() => ({}));
|
|
986
2358
|
if (!res.ok && payload?.success !== true) {
|
|
987
2359
|
const message = payload?.error?.message || payload?.error_description || payload?.error || `HTTP ${res.status}`;
|
|
@@ -1001,8 +2373,20 @@ function useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo) {
|
|
|
1001
2373
|
}
|
|
1002
2374
|
let cancelled = false;
|
|
1003
2375
|
setLoading(true);
|
|
1004
|
-
const
|
|
1005
|
-
fetch(
|
|
2376
|
+
const url2 = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/public/apps/${encodeURIComponent(appKey)}/sign-in-context?return_to=${encodeURIComponent(returnTo)}`;
|
|
2377
|
+
fetch(url2, { credentials: "include" }).then(async (r) => {
|
|
2378
|
+
const contentType = r.headers.get("content-type") || "";
|
|
2379
|
+
if (!r.ok || !contentType.includes("json")) {
|
|
2380
|
+
const bodyPreview = await r.text().then((t2) => t2.slice(0, 160)).catch(() => "");
|
|
2381
|
+
console.error(
|
|
2382
|
+
`[IQAuth] sign-in-context request failed: ${r.status} ${r.statusText} (content-type: ${contentType || "\u2014"}). URL: ${url2}. Common causes: (1) iqAuthBaseUrl points at the wrong host (it should be your IQAuth issuer, e.g. https://auth.dispositioniq.com \u2014 NOT your own app's domain); (2) the app key "${appKey}" is wrong or revoked; (3) CORS preflight is blocked because this origin isn't in the app's allowed-origins list. Response body preview: ${bodyPreview}`
|
|
2383
|
+
);
|
|
2384
|
+
throw new Error(
|
|
2385
|
+
r.status >= 500 ? "Failed to load sign-in context (server error)" : `Failed to load sign-in context (HTTP ${r.status})`
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
return r.json();
|
|
2389
|
+
}).then((payload) => {
|
|
1006
2390
|
if (cancelled) return;
|
|
1007
2391
|
if (payload?.success === false) throw new Error(payload?.error?.message || "Failed to load sign-in context");
|
|
1008
2392
|
setCtx(payload.data);
|
|
@@ -1025,6 +2409,14 @@ var SHELL_CSS = `
|
|
|
1025
2409
|
grid-template-columns: 1fr;
|
|
1026
2410
|
background: var(--brand-bg, #f7f7f6);
|
|
1027
2411
|
color: var(--brand-text, #0f172a);
|
|
2412
|
+
/* Container queries so the two-pane layout responds to the SDK's
|
|
2413
|
+
RENDERED width, not the viewport. This keeps the form usable when
|
|
2414
|
+
a host app embeds <SignIn/> inside a narrower card or sidebar
|
|
2415
|
+
instead of full-screen \u2014 the previous viewport @media query would
|
|
2416
|
+
happily render the side-by-side hero+form into a 200px container
|
|
2417
|
+
and wrap text one character per line. */
|
|
2418
|
+
container-type: inline-size;
|
|
2419
|
+
container-name: iqauth-sdk;
|
|
1028
2420
|
}
|
|
1029
2421
|
.iqauth-sdk-hero { display: none; }
|
|
1030
2422
|
.iqauth-sdk-pane {
|
|
@@ -1074,7 +2466,7 @@ var SHELL_CSS = `
|
|
|
1074
2466
|
.iqauth-sdk-google-btn:hover { background: #f8fafc; border-color: rgba(15,23,42,0.28); }
|
|
1075
2467
|
.iqauth-sdk-google-btn[disabled] { opacity: 0.6; cursor: not-allowed; }
|
|
1076
2468
|
|
|
1077
|
-
@
|
|
2469
|
+
@container iqauth-sdk (min-width: 768px) {
|
|
1078
2470
|
.iqauth-sdk-shell:not([data-layout="centered_card"]):not([data-layout="full_bleed"]) { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
|
1079
2471
|
.iqauth-sdk-hero {
|
|
1080
2472
|
display: flex; flex-direction: column; justify-content: space-between;
|
|
@@ -1095,7 +2487,7 @@ var SHELL_CSS = `
|
|
|
1095
2487
|
.iqauth-sdk-hero-foot { font-size: 12px; opacity: 0.7; }
|
|
1096
2488
|
.iqauth-sdk-mobile-brand { display: none; }
|
|
1097
2489
|
}
|
|
1098
|
-
@
|
|
2490
|
+
@container iqauth-sdk (min-width: 1280px) {
|
|
1099
2491
|
.iqauth-sdk-shell:not([data-layout="centered_card"]):not([data-layout="full_bleed"]) { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
|
|
1100
2492
|
}
|
|
1101
2493
|
`;
|
|
@@ -1164,34 +2556,34 @@ function flattenBrandingPayload(data) {
|
|
|
1164
2556
|
function useResolvedSdkBranding(iqAuthBaseUrl, appId) {
|
|
1165
2557
|
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
1166
2558
|
const resolvedAppId = appId ?? ctx?.manager?.appKey ?? null;
|
|
1167
|
-
const
|
|
1168
|
-
const cached = sdkBrandingCache.get(
|
|
2559
|
+
const url2 = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/public/branding${resolvedAppId ? `?appId=${encodeURIComponent(resolvedAppId)}` : ""}`;
|
|
2560
|
+
const cached = sdkBrandingCache.get(url2);
|
|
1169
2561
|
const fresh = cached && Date.now() - cached.ts < SDK_BRANDING_TTL_MS ? cached.data : null;
|
|
1170
2562
|
const [b, setB] = (0, import_react.useState)(fresh);
|
|
1171
2563
|
(0, import_react.useEffect)(() => {
|
|
1172
2564
|
let cancelled = false;
|
|
1173
|
-
const entry = sdkBrandingCache.get(
|
|
1174
|
-
const
|
|
1175
|
-
if (entry?.rev)
|
|
2565
|
+
const entry = sdkBrandingCache.get(url2);
|
|
2566
|
+
const headers2 = {};
|
|
2567
|
+
if (entry?.rev) headers2["If-None-Match"] = `W/"brand-${entry.rev}"`;
|
|
1176
2568
|
if (entry) setB(entry.data);
|
|
1177
|
-
fetch(
|
|
2569
|
+
fetch(url2, { credentials: "include", headers: headers2 }).then(async (r) => {
|
|
1178
2570
|
if (cancelled) return;
|
|
1179
2571
|
if (r.status === 304 && entry) {
|
|
1180
|
-
sdkBrandingCache.set(
|
|
2572
|
+
sdkBrandingCache.set(url2, { ...entry, ts: Date.now() });
|
|
1181
2573
|
return;
|
|
1182
2574
|
}
|
|
1183
2575
|
if (!r.ok) return;
|
|
1184
2576
|
const p = await r.json().catch(() => null);
|
|
1185
2577
|
if (!p?.data) return;
|
|
1186
2578
|
const flat = flattenBrandingPayload(p.data);
|
|
1187
|
-
sdkBrandingCache.set(
|
|
2579
|
+
sdkBrandingCache.set(url2, { ts: Date.now(), rev: flat.brandingRev || "", data: flat });
|
|
1188
2580
|
setB(flat);
|
|
1189
2581
|
}).catch(() => {
|
|
1190
2582
|
});
|
|
1191
2583
|
return () => {
|
|
1192
2584
|
cancelled = true;
|
|
1193
2585
|
};
|
|
1194
|
-
}, [
|
|
2586
|
+
}, [url2]);
|
|
1195
2587
|
return b;
|
|
1196
2588
|
}
|
|
1197
2589
|
function ensureSdkShellStyles() {
|
|
@@ -1232,10 +2624,12 @@ function Shell({
|
|
|
1232
2624
|
className,
|
|
1233
2625
|
children,
|
|
1234
2626
|
title,
|
|
1235
|
-
subtitle
|
|
2627
|
+
subtitle,
|
|
2628
|
+
appearance
|
|
1236
2629
|
}) {
|
|
1237
2630
|
ensureSdkShellStyles();
|
|
1238
|
-
|
|
2631
|
+
const t2 = useT();
|
|
2632
|
+
useDocumentBranding(branding, title || t2("signIn.title"));
|
|
1239
2633
|
const brandVars = brandStyle(branding);
|
|
1240
2634
|
const brandName = branding?.brandName || "IQAuth";
|
|
1241
2635
|
const heroImage = branding?.heroImageUrl || null;
|
|
@@ -1246,14 +2640,16 @@ function Shell({
|
|
|
1246
2640
|
const heroStyle = heroImage ? { ["--iqauth-sdk-hero-image"]: `url("${heroImage.replace(/"/g, '\\"')}")` } : {};
|
|
1247
2641
|
const shellStyle = {
|
|
1248
2642
|
...brandVars,
|
|
1249
|
-
...layout === "full_bleed" && bgImage ? { backgroundImage: `linear-gradient(rgba(15,23,42,0.35), rgba(15,23,42,0.45)), url("${bgImage.replace(/"/g, '\\"')}")` } : {}
|
|
2643
|
+
...layout === "full_bleed" && bgImage ? { backgroundImage: `linear-gradient(rgba(15,23,42,0.35), rgba(15,23,42,0.45)), url("${bgImage.replace(/"/g, '\\"')}")` } : {},
|
|
2644
|
+
...appearance?.elements?.rootBox?.style || {}
|
|
1250
2645
|
};
|
|
2646
|
+
const ap = appearance?.elements;
|
|
1251
2647
|
const supportLink = branding?.supportUrl ? branding.supportUrl : branding?.supportEmail ? `mailto:${branding.supportEmail}` : null;
|
|
1252
2648
|
const hasFooterLinks = !!(branding?.termsUrl || branding?.privacyUrl || supportLink);
|
|
1253
2649
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1254
2650
|
"div",
|
|
1255
2651
|
{
|
|
1256
|
-
className: `iqauth-sdk-shell${className ? ` ${className}` : ""}`,
|
|
2652
|
+
className: `iqauth-sdk-shell${className ? ` ${className}` : ""}${ap?.rootBox?.className ? ` ${ap.rootBox.className}` : ""}`,
|
|
1257
2653
|
"data-layout": layout,
|
|
1258
2654
|
"data-social-style": socialStyle || void 0,
|
|
1259
2655
|
style: shellStyle,
|
|
@@ -1274,13 +2670,34 @@ function Shell({
|
|
|
1274
2670
|
] }),
|
|
1275
2671
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "iqauth-sdk-pane", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", { style: { width: "100%", maxWidth: 420 }, children: [
|
|
1276
2672
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "iqauth-sdk-mobile-brand", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SdkBrandLogo, { branding, alt: `${brandName} logo`, fallback: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: brandName }) }) }),
|
|
1277
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2673
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2674
|
+
"section",
|
|
2675
|
+
{
|
|
2676
|
+
className: `iqauth-sdk-card${ap?.card?.className ? ` ${ap.card.className}` : ""}`,
|
|
2677
|
+
style: ap?.card?.style,
|
|
2678
|
+
children: [
|
|
2679
|
+
title || subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2680
|
+
"div",
|
|
2681
|
+
{
|
|
2682
|
+
className: `iqauth-sdk-card-header${ap?.cardHeader?.className ? ` ${ap.cardHeader.className}` : ""}`,
|
|
2683
|
+
style: ap?.cardHeader?.style,
|
|
2684
|
+
children: [
|
|
2685
|
+
title ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: ap?.headerTitle?.className, style: ap?.headerTitle?.style, children: title }) : null,
|
|
2686
|
+
subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: ap?.headerSubtitle?.className, style: ap?.headerSubtitle?.style, children: subtitle }) : null
|
|
2687
|
+
]
|
|
2688
|
+
}
|
|
2689
|
+
) : null,
|
|
2690
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2691
|
+
"div",
|
|
2692
|
+
{
|
|
2693
|
+
className: `iqauth-sdk-card-body${ap?.cardBody?.className ? ` ${ap.cardBody.className}` : ""}`,
|
|
2694
|
+
style: ap?.cardBody?.style,
|
|
2695
|
+
children
|
|
2696
|
+
}
|
|
2697
|
+
)
|
|
2698
|
+
]
|
|
2699
|
+
}
|
|
2700
|
+
),
|
|
1284
2701
|
hasFooterLinks || branding?.footerText ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("footer", { className: "iqauth-sdk-footer", children: [
|
|
1285
2702
|
branding?.footerText ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "text-brand-footer-sdk", style: { fontSize: 12, opacity: 0.75 }, children: branding.footerText }) : null,
|
|
1286
2703
|
hasFooterLinks ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "iqauth-sdk-footer-links", children: [
|
|
@@ -1371,8 +2788,30 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
|
|
|
1371
2788
|
if (!ctx.returnAllowed) return false;
|
|
1372
2789
|
return true;
|
|
1373
2790
|
}
|
|
1374
|
-
function SignIn(
|
|
2791
|
+
function SignIn(props) {
|
|
2792
|
+
const providerCtx = (0, import_react.useContext)(IQAuthContext);
|
|
2793
|
+
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.issuerUrl ?? "";
|
|
2794
|
+
const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
|
|
2795
|
+
const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
|
|
2796
|
+
const { onRedirect, className, prompt, appearance: instanceAppearance } = props;
|
|
2797
|
+
const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
|
|
2798
|
+
if (!iqAuthBaseUrl || !appKey) {
|
|
2799
|
+
console.error(
|
|
2800
|
+
"[IQAuth] <SignIn /> could not determine iqAuthBaseUrl/appKey. Either pass them explicitly OR wrap the component in <IQAuthProvider publishableKey=\u2026/>."
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
const t2 = useT();
|
|
2804
|
+
const localeBundle = useLocale();
|
|
1375
2805
|
const { ctx, loading, error } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo);
|
|
2806
|
+
const preflightLoggedRef = (0, import_react.useRef)(false);
|
|
2807
|
+
(0, import_react.useEffect)(() => {
|
|
2808
|
+
if (!ctx || preflightLoggedRef.current) return;
|
|
2809
|
+
if (ctx.returnAllowed) return;
|
|
2810
|
+
preflightLoggedRef.current = true;
|
|
2811
|
+
console.error(
|
|
2812
|
+
`[IQAuth] returnTo "${returnTo}" is NOT in the app's allowed origins. Add it via the IQAuth admin console: Apps \u2192 ${ctx.app.key} \u2192 Allowed Origins. Currently allowed: [${ctx.allowedOrigins.join(", ") || "\u2014"}].`
|
|
2813
|
+
);
|
|
2814
|
+
}, [ctx, returnTo]);
|
|
1376
2815
|
const [email, setEmail] = (0, import_react.useState)("");
|
|
1377
2816
|
const [password, setPassword] = (0, import_react.useState)("");
|
|
1378
2817
|
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
@@ -1431,9 +2870,9 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1431
2870
|
body: JSON.stringify({ email, password, ...oidcPayload() })
|
|
1432
2871
|
});
|
|
1433
2872
|
const payload = await r.json().catch(() => ({}));
|
|
1434
|
-
if (!handlePayload(payload)) setFormError(
|
|
2873
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1435
2874
|
} catch (err) {
|
|
1436
|
-
setFormError(err.message ||
|
|
2875
|
+
setFormError(err.message || t(localeBundle, "errors.network"));
|
|
1437
2876
|
}
|
|
1438
2877
|
setSubmitting(false);
|
|
1439
2878
|
};
|
|
@@ -1455,7 +2894,7 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1455
2894
|
})
|
|
1456
2895
|
});
|
|
1457
2896
|
const payload = await r.json().catch(() => ({}));
|
|
1458
|
-
if (!handlePayload(payload)) setFormError(
|
|
2897
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1459
2898
|
setSubmitting(false);
|
|
1460
2899
|
};
|
|
1461
2900
|
const submitTenant = async (tenantId) => {
|
|
@@ -1469,7 +2908,7 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1469
2908
|
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
|
|
1470
2909
|
});
|
|
1471
2910
|
const payload = await r.json().catch(() => ({}));
|
|
1472
|
-
if (!handlePayload(payload)) setFormError(
|
|
2911
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1473
2912
|
setSubmitting(false);
|
|
1474
2913
|
};
|
|
1475
2914
|
const startGoogleLogin = () => {
|
|
@@ -1478,8 +2917,8 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1478
2917
|
return;
|
|
1479
2918
|
}
|
|
1480
2919
|
const bridgeUrl = window.location.href;
|
|
1481
|
-
const
|
|
1482
|
-
window.location.href =
|
|
2920
|
+
const url2 = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/google?redirect_uri=${encodeURIComponent(bridgeUrl)}&client_id=${encodeURIComponent(ctx.app.defaultClientId)}`;
|
|
2921
|
+
window.location.href = url2;
|
|
1483
2922
|
};
|
|
1484
2923
|
(0, import_react.useEffect)(() => {
|
|
1485
2924
|
if (loading || error || !ctx) return;
|
|
@@ -1567,36 +3006,36 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1567
3006
|
setOauthExchanging(false);
|
|
1568
3007
|
})();
|
|
1569
3008
|
}, [ctx?.app.defaultClientId]);
|
|
1570
|
-
if (loading || oauthExchanging) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: ctx?.branding || null, className, title: oauthExchanging ? "
|
|
1571
|
-
if (error || !ctx) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: null, className, title: "
|
|
1572
|
-
if (!ctx.returnAllowed) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: ctx.branding, className, title: "
|
|
3009
|
+
if (loading || oauthExchanging) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding: ctx?.branding || null, className, title: oauthExchanging ? t2("signIn.submitting") : t2("common.loading"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: oauthExchanging ? t2("signIn.submitting") : t2("common.loading") }) });
|
|
3010
|
+
if (error || !ctx) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding: null, className, title: t2("errors.serverError"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error || t2("errors.serverError") }) });
|
|
3011
|
+
if (!ctx.returnAllowed) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding: ctx.branding, className, title: t2("errors.generic"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: `returnTo "${returnTo}" is not in this app's allowed origins.` }) });
|
|
1573
3012
|
const silentEligible = isSilentSsoEligible(ctx, effectivePrompt);
|
|
1574
3013
|
if (silentEligible && silent !== "failed") {
|
|
1575
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding: ctx.branding, className, title: "
|
|
1576
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-silent-resume", style: { fontSize: 14, opacity: 0.8 }, children: "
|
|
1577
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", style: { fontSize: 13 }, children: "
|
|
3014
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { appearance, branding: ctx.branding, className, title: t2("signIn.resumingSession"), subtitle: ctx.session ? `${t2("signIn.subtitle")}, ${ctx.session.name || ctx.session.email}.` : void 0, children: [
|
|
3015
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-silent-resume", style: { fontSize: 14, opacity: 0.8 }, children: t2("signIn.resumingSession") }),
|
|
3016
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", style: { fontSize: 13 }, children: t2("signIn.useDifferentAccount") })
|
|
1578
3017
|
] });
|
|
1579
3018
|
}
|
|
1580
|
-
const cardTitle = ctx.branding?.loginHeadline ||
|
|
3019
|
+
const cardTitle = ctx.branding?.loginHeadline || t2("signIn.titleWithApp", { appName: ctx.app.name });
|
|
1581
3020
|
const cardSubtitle = ctx.branding?.loginSubheadline || void 0;
|
|
1582
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding: ctx.branding, className, title: cardTitle, subtitle: cardSubtitle, children: [
|
|
3021
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { appearance, branding: ctx.branding, className, title: cardTitle, subtitle: cardSubtitle, children: [
|
|
1583
3022
|
formError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: formError }) : null,
|
|
1584
|
-
tenantSel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-label": "
|
|
3023
|
+
tenantSel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-label": t2("signIn.selectTenant"), style: { display: "flex", flexDirection: "column", gap: 8 }, children: tenantSel.tenants.map((tn) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1585
3024
|
"button",
|
|
1586
3025
|
{
|
|
1587
3026
|
type: "button",
|
|
1588
|
-
"data-iqauth-tenant":
|
|
1589
|
-
onClick: () => submitTenant(
|
|
3027
|
+
"data-iqauth-tenant": tn.tenantId,
|
|
3028
|
+
onClick: () => submitTenant(tn.tenantId),
|
|
1590
3029
|
style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
|
|
1591
3030
|
children: [
|
|
1592
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children:
|
|
1593
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children:
|
|
3031
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: tn.tenantName || tn.tenantSlug || tn.tenantId }),
|
|
3032
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: tn.roles.join(", ") })
|
|
1594
3033
|
]
|
|
1595
3034
|
},
|
|
1596
|
-
|
|
1597
|
-
)) }) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": "
|
|
1598
|
-
!mfa.backup && mfa.methods.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "
|
|
1599
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? "
|
|
3035
|
+
tn.tenantId
|
|
3036
|
+
)) }) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
|
|
3037
|
+
!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,
|
|
3038
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1600
3039
|
"input",
|
|
1601
3040
|
{
|
|
1602
3041
|
style: { ...inputStyle(), fontFamily: "monospace", textAlign: mfa.backup ? "left" : "center", letterSpacing: mfa.backup ? "0.04em" : "0.3em" },
|
|
@@ -1606,34 +3045,33 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1606
3045
|
inputMode: mfa.backup ? "text" : "numeric"
|
|
1607
3046
|
}
|
|
1608
3047
|
) }),
|
|
1609
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !mfa.code, children: submitting ? "
|
|
1610
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(GhostButton, { type: "button", onClick: () => setMfa({ ...mfa, backup: !mfa.backup, code: "" }), children: mfa.backup ? "
|
|
3048
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !mfa.code, children: submitting ? t2("mfa.submitting") : t2("mfa.submit") }),
|
|
3049
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(GhostButton, { type: "button", onClick: () => setMfa({ ...mfa, backup: !mfa.backup, code: "" }), children: mfa.backup ? t2("mfa.useAuthenticator") : t2("mfa.useBackupCode") })
|
|
1611
3050
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
1612
3051
|
ctx.providers?.google ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1613
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "iqauth-sdk-google-btn", onClick: startGoogleLogin, disabled: submitting, "aria-label": ctx.branding?.googleButtonLabel || "
|
|
3052
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "iqauth-sdk-google-btn", onClick: startGoogleLogin, disabled: submitting, "aria-label": ctx.branding?.googleButtonLabel || t2("signIn.continueWithGoogle"), children: [
|
|
1614
3053
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": "true", children: [
|
|
1615
3054
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "#4285F4", d: "M17.64 9.2c0-.64-.06-1.25-.17-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.71v2.26h2.92a8.78 8.78 0 0 0 2.68-6.61z" }),
|
|
1616
3055
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "#34A853", d: "M9 18c2.43 0 4.47-.81 5.96-2.18l-2.92-2.26a5.4 5.4 0 0 1-8.04-2.83H.96v2.33A9 9 0 0 0 9 18z" }),
|
|
1617
3056
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "#FBBC05", d: "M3.96 10.71A5.41 5.41 0 0 1 3.68 9c0-.59.1-1.17.29-1.71V4.96H.96a9 9 0 0 0 0 8.08l3-2.33z" }),
|
|
1618
3057
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "#EA4335", d: "M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.59A9 9 0 0 0 .96 4.96l3 2.33A5.4 5.4 0 0 1 9 3.58z" })
|
|
1619
3058
|
] }),
|
|
1620
|
-
ctx.branding?.googleButtonLabel || "
|
|
3059
|
+
ctx.branding?.googleButtonLabel || t2("signIn.continueWithGoogle")
|
|
1621
3060
|
] }),
|
|
1622
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", "aria-label": "or", className: "iqauth-sdk-divider", children: "
|
|
3061
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", "aria-label": t2("common.or"), className: "iqauth-sdk-divider", children: t2("signIn.dividerOr").toUpperCase() })
|
|
1623
3062
|
] }) : null,
|
|
1624
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitLogin, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label":
|
|
1625
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "
|
|
1626
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "
|
|
1627
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password, children: submitting ? "
|
|
3063
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitLogin, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("signIn.titleWithApp", { appName: ctx.app.name }), children: [
|
|
3064
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("signIn.emailLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "email", autoComplete: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value) }) }),
|
|
3065
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("signIn.passwordLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "password", autoComplete: "current-password", required: true, value: password, onChange: (e) => setPassword(e.target.value) }) }),
|
|
3066
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password, children: submitting ? t2("signIn.submitting") : t2("signIn.submit") })
|
|
1628
3067
|
] })
|
|
1629
3068
|
] }),
|
|
1630
|
-
(silent === "failed" || effectivePrompt === "login" && ctx.session) && !mfa ? /* @__PURE__ */ (0, import_jsx_runtime.
|
|
1631
|
-
silent === "failed" && ctx.session ? "Couldn't resume your session. " : null,
|
|
1632
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", children: "Use a different account" })
|
|
1633
|
-
] }) : null
|
|
3069
|
+
(silent === "failed" || effectivePrompt === "login" && ctx.session) && !mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { marginTop: 12, fontSize: 13, opacity: 0.75 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", children: t2("signIn.useDifferentAccount") }) }) : null
|
|
1634
3070
|
] });
|
|
1635
3071
|
}
|
|
1636
3072
|
function SignUp({ iqAuthBaseUrl, appKey, returnTo, onSuccess, className }) {
|
|
3073
|
+
const t2 = useT();
|
|
3074
|
+
const localeBundle = useLocale();
|
|
1637
3075
|
const { ctx, loading } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo || "");
|
|
1638
3076
|
const [name, setName] = (0, import_react.useState)("");
|
|
1639
3077
|
const [email, setEmail] = (0, import_react.useState)("");
|
|
@@ -1655,22 +3093,19 @@ function SignUp({ iqAuthBaseUrl, appKey, returnTo, onSuccess, className }) {
|
|
|
1655
3093
|
setDone(true);
|
|
1656
3094
|
onSuccess?.();
|
|
1657
3095
|
} catch (err) {
|
|
1658
|
-
setError(err.message);
|
|
3096
|
+
setError(localizeError(localeBundle, err.message));
|
|
1659
3097
|
}
|
|
1660
3098
|
setSubmitting(false);
|
|
1661
3099
|
};
|
|
1662
|
-
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: null, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "
|
|
1663
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
1664
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password || !name, children: submitting ? "Creating\u2026" : "Create account" })
|
|
1672
|
-
] })
|
|
1673
|
-
] });
|
|
3100
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: null, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t2("common.loading") }) });
|
|
3101
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: ctx?.branding || null, className, title: t2("signUp.title"), children: done ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t2("magicLink.subtitle", { email }) }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
3102
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
3103
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("signUp.nameLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), value: name, onChange: (e) => setName(e.target.value), required: true }) }),
|
|
3104
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("signUp.emailLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true }) }),
|
|
3105
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: `${t2("signUp.tenantNameLabel")} (${t2("common.optional")})`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), value: organizationName, onChange: (e) => setOrganizationName(e.target.value) }) }),
|
|
3106
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("signUp.passwordLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "password", autoComplete: "new-password", minLength: 8, value: password, onChange: (e) => setPassword(e.target.value), required: true }) }),
|
|
3107
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password || !name, children: submitting ? t2("signUp.submitting") : t2("signUp.submit") })
|
|
3108
|
+
] }) });
|
|
1674
3109
|
}
|
|
1675
3110
|
function initialsOf(name, email) {
|
|
1676
3111
|
const src = name || email || "?";
|
|
@@ -1679,6 +3114,7 @@ function initialsOf(name, email) {
|
|
|
1679
3114
|
return src.substring(0, 2).toUpperCase();
|
|
1680
3115
|
}
|
|
1681
3116
|
function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
3117
|
+
const t2 = useT();
|
|
1682
3118
|
const [user, setUser] = (0, import_react.useState)(null);
|
|
1683
3119
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1684
3120
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
@@ -1749,7 +3185,7 @@ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
|
1749
3185
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500, color: "#0f172a" }, children: user.name }),
|
|
1750
3186
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: user.email })
|
|
1751
3187
|
] }),
|
|
1752
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: target, role: "menuitem", style: { display: "block", padding: "8px 10px", fontSize: 13, color: primary, textDecoration: "none" }, children: "
|
|
3188
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: target, role: "menuitem", style: { display: "block", padding: "8px 10px", fontSize: 13, color: primary, textDecoration: "none" }, children: t2("userButton.manageAccount") }),
|
|
1753
3189
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1754
3190
|
"button",
|
|
1755
3191
|
{
|
|
@@ -1757,7 +3193,7 @@ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
|
1757
3193
|
type: "button",
|
|
1758
3194
|
onClick: signOut2,
|
|
1759
3195
|
style: { display: "block", width: "100%", textAlign: "left", padding: "8px 10px", fontSize: 13, background: "transparent", border: "none", cursor: "pointer", color: "#b91c1c" },
|
|
1760
|
-
children: "
|
|
3196
|
+
children: t2("userButton.signOut")
|
|
1761
3197
|
}
|
|
1762
3198
|
)
|
|
1763
3199
|
] }) : null
|
|
@@ -1766,185 +3202,1187 @@ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
|
1766
3202
|
);
|
|
1767
3203
|
}
|
|
1768
3204
|
function UserProfile({ iqAuthBaseUrl, className }) {
|
|
3205
|
+
const t2 = useT();
|
|
3206
|
+
const localeBundle = useLocale();
|
|
1769
3207
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
1770
3208
|
const [user, setUser] = (0, import_react.useState)(null);
|
|
1771
3209
|
const [oldPassword, setOldPassword] = (0, import_react.useState)("");
|
|
1772
3210
|
const [newPassword, setNewPassword] = (0, import_react.useState)("");
|
|
1773
3211
|
const [pwState, setPwState] = (0, import_react.useState)({ submitting: false, message: "", error: "" });
|
|
1774
3212
|
const [sessions, setSessions] = (0, import_react.useState)([]);
|
|
3213
|
+
const [revokeAllBusy, setRevokeAllBusy] = (0, import_react.useState)(false);
|
|
3214
|
+
const loadSessions = () => {
|
|
3215
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions`, { credentials: "include" }).then((r) => r.ok ? r.json() : Promise.reject(r)).then((p) => setSessions(p?.data?.sessions || [])).catch(() => {
|
|
3216
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions`, { credentials: "include" }).then((r) => r.json()).then((p) => setSessions(p?.data?.sessions || p?.data || [])).catch(() => {
|
|
3217
|
+
});
|
|
3218
|
+
});
|
|
3219
|
+
};
|
|
3220
|
+
(0, import_react.useEffect)(() => {
|
|
3221
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
3222
|
+
if (p?.data) setUser(p.data);
|
|
3223
|
+
}).catch(() => {
|
|
3224
|
+
});
|
|
3225
|
+
loadSessions();
|
|
3226
|
+
}, [iqAuthBaseUrl]);
|
|
3227
|
+
const changePassword = async (e) => {
|
|
3228
|
+
e.preventDefault();
|
|
3229
|
+
setPwState({ submitting: true, message: "", error: "" });
|
|
3230
|
+
try {
|
|
3231
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/password/change`, {
|
|
3232
|
+
method: "POST",
|
|
3233
|
+
headers: { "Content-Type": "application/json" },
|
|
3234
|
+
body: JSON.stringify({ oldPassword, newPassword })
|
|
3235
|
+
});
|
|
3236
|
+
setPwState({ submitting: false, message: t(localeBundle, "userProfile.passwordUpdated"), error: "" });
|
|
3237
|
+
setOldPassword("");
|
|
3238
|
+
setNewPassword("");
|
|
3239
|
+
} catch (err) {
|
|
3240
|
+
setPwState({ submitting: false, message: "", error: localizeError(localeBundle, err.message) });
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
const revoke = async (sessionId) => {
|
|
3244
|
+
const newRes = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
|
|
3245
|
+
if (!newRes.ok && newRes.status === 404) {
|
|
3246
|
+
await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
|
|
3247
|
+
}
|
|
3248
|
+
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
|
3249
|
+
};
|
|
3250
|
+
const revokeAllOthers = async () => {
|
|
3251
|
+
setRevokeAllBusy(true);
|
|
3252
|
+
try {
|
|
3253
|
+
await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions/revoke-all-others`, { method: "POST", credentials: "include" });
|
|
3254
|
+
setSessions((prev) => prev.filter((s) => s.isCurrent));
|
|
3255
|
+
} finally {
|
|
3256
|
+
setRevokeAllBusy(false);
|
|
3257
|
+
}
|
|
3258
|
+
};
|
|
3259
|
+
if (!user) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t2("common.loading") }) });
|
|
3260
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding, className, title: t2("userProfile.title"), children: [
|
|
3261
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-profile", style: { marginBottom: 20 }, children: [
|
|
3262
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-profile", style: { fontSize: 14, fontWeight: 600 }, children: t2("userProfile.profileTab") }),
|
|
3263
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
|
|
3264
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
3265
|
+
t2("common.name"),
|
|
3266
|
+
":"
|
|
3267
|
+
] }),
|
|
3268
|
+
" ",
|
|
3269
|
+
user.name
|
|
3270
|
+
] }),
|
|
3271
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
|
|
3272
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
3273
|
+
t2("common.email"),
|
|
3274
|
+
":"
|
|
3275
|
+
] }),
|
|
3276
|
+
" ",
|
|
3277
|
+
user.email
|
|
3278
|
+
] })
|
|
3279
|
+
] }),
|
|
3280
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-pw", style: { marginBottom: 20 }, children: [
|
|
3281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-pw", style: { fontSize: 14, fontWeight: 600 }, children: t2("userProfile.changePassword") }),
|
|
3282
|
+
pwState.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: pwState.error }) : null,
|
|
3283
|
+
pwState.message ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", style: { fontSize: 13, color: "#047857" }, children: pwState.message }) : null,
|
|
3284
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: changePassword, style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
|
|
3285
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("userProfile.currentPassword"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "password", autoComplete: "current-password", value: oldPassword, onChange: (e) => setOldPassword(e.target.value), required: true }) }),
|
|
3286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: t2("userProfile.newPassword"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "password", autoComplete: "new-password", minLength: 8, value: newPassword, onChange: (e) => setNewPassword(e.target.value), required: true }) }),
|
|
3287
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: pwState.submitting || !oldPassword || !newPassword, children: pwState.submitting ? t2("common.saving") : t2("userProfile.changePassword") })
|
|
3288
|
+
] })
|
|
3289
|
+
] }),
|
|
3290
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-sessions", children: [
|
|
3291
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
|
|
3292
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-sessions", style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: t2("userProfile.sessionsTab") }),
|
|
3293
|
+
sessions.some((s) => !s.isCurrent) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3294
|
+
"button",
|
|
3295
|
+
{
|
|
3296
|
+
type: "button",
|
|
3297
|
+
disabled: revokeAllBusy,
|
|
3298
|
+
onClick: revokeAllOthers,
|
|
3299
|
+
style: { fontSize: 12, color: "#b91c1c", background: "transparent", border: "1px solid #fecaca", borderRadius: 4, padding: "4px 10px", cursor: "pointer" },
|
|
3300
|
+
children: revokeAllBusy ? t2("common.submitting") : t2("userProfile.revokeAllOthers")
|
|
3301
|
+
}
|
|
3302
|
+
)
|
|
3303
|
+
] }),
|
|
3304
|
+
sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6 }, children: t2("userProfile.sessionsEmpty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: "8px 0 0", display: "flex", flexDirection: "column", gap: 6 }, children: sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13, padding: "8px 10px", background: s.isCurrent ? "#ecfdf5" : "#f8fafc", borderRadius: 6, border: s.isCurrent ? "1px solid #a7f3d0" : "1px solid transparent" }, children: [
|
|
3305
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: [
|
|
3306
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontWeight: 500 }, children: [
|
|
3307
|
+
s.device || s.userAgent || s.deviceName || "\u2014",
|
|
3308
|
+
s.isCurrent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: 8, fontSize: 11, color: "#047857" }, children: [
|
|
3309
|
+
"(",
|
|
3310
|
+
t2("userProfile.thisDevice"),
|
|
3311
|
+
")"
|
|
3312
|
+
] })
|
|
3313
|
+
] }),
|
|
3314
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: 11, opacity: 0.65 }, children: [
|
|
3315
|
+
s.ip || "\u2014",
|
|
3316
|
+
s.lastActiveAt ? ` \xB7 ${new Date(s.lastActiveAt).toLocaleString()}` : ""
|
|
3317
|
+
] })
|
|
3318
|
+
] }),
|
|
3319
|
+
!s.isCurrent && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => revoke(s.id), style: { fontSize: 12, color: "#b91c1c", background: "transparent", border: "1px solid #fecaca", borderRadius: 4, padding: "2px 8px", cursor: "pointer" }, children: t2("userProfile.revokeSession") })
|
|
3320
|
+
] }, s.id)) })
|
|
3321
|
+
] })
|
|
3322
|
+
] });
|
|
3323
|
+
}
|
|
3324
|
+
function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearance, className }) {
|
|
3325
|
+
const t2 = useT();
|
|
3326
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3327
|
+
const accent = branding?.accentColor || "#6366f1";
|
|
3328
|
+
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
3329
|
+
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
3330
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
3331
|
+
(0, import_react.useEffect)(() => {
|
|
3332
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
3333
|
+
if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
|
|
3334
|
+
}).catch(() => {
|
|
3335
|
+
});
|
|
3336
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json()).then((p) => setMemberships(p?.data?.memberships || p?.data || [])).catch(() => {
|
|
3337
|
+
});
|
|
3338
|
+
}, [iqAuthBaseUrl]);
|
|
3339
|
+
const switchTo = async (tenantId) => {
|
|
3340
|
+
try {
|
|
3341
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
|
|
3342
|
+
method: "POST",
|
|
3343
|
+
headers: { "Content-Type": "application/json" },
|
|
3344
|
+
body: JSON.stringify({ tenantId })
|
|
3345
|
+
});
|
|
3346
|
+
setActiveTenantId(tenantId);
|
|
3347
|
+
setOpen(false);
|
|
3348
|
+
onSwitched?.(tenantId);
|
|
3349
|
+
} catch {
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
const active = memberships.find((m) => m.tenantId === activeTenantId);
|
|
3353
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3354
|
+
"div",
|
|
3355
|
+
{
|
|
3356
|
+
className,
|
|
3357
|
+
style: { position: "relative", display: "inline-block", ...brandStyle(branding) },
|
|
3358
|
+
"data-iqauth-sdk-orgswitcher": "",
|
|
3359
|
+
"data-branding-rev": branding?.brandingRev || "",
|
|
3360
|
+
children: [
|
|
3361
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3362
|
+
"button",
|
|
3363
|
+
{
|
|
3364
|
+
type: "button",
|
|
3365
|
+
"aria-haspopup": "menu",
|
|
3366
|
+
"aria-expanded": open,
|
|
3367
|
+
onClick: () => setOpen((o) => !o),
|
|
3368
|
+
style: { background: "transparent", border: `1px solid ${accent}55`, color: branding?.primaryColor || "#0f172a", padding: "6px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13 },
|
|
3369
|
+
children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
|
|
3370
|
+
}
|
|
3371
|
+
),
|
|
3372
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", style: {
|
|
3373
|
+
position: "absolute",
|
|
3374
|
+
left: 0,
|
|
3375
|
+
top: 36,
|
|
3376
|
+
minWidth: 220,
|
|
3377
|
+
background: "#fff",
|
|
3378
|
+
border: "1px solid rgba(15,23,42,0.12)",
|
|
3379
|
+
borderRadius: 8,
|
|
3380
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
3381
|
+
padding: 8,
|
|
3382
|
+
zIndex: 100
|
|
3383
|
+
}, children: 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)(
|
|
3384
|
+
"button",
|
|
3385
|
+
{
|
|
3386
|
+
role: "menuitem",
|
|
3387
|
+
type: "button",
|
|
3388
|
+
onClick: () => switchTo(m.tenantId),
|
|
3389
|
+
style: {
|
|
3390
|
+
display: "block",
|
|
3391
|
+
width: "100%",
|
|
3392
|
+
textAlign: "left",
|
|
3393
|
+
padding: "8px 10px",
|
|
3394
|
+
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
3395
|
+
border: "none",
|
|
3396
|
+
borderRadius: 4,
|
|
3397
|
+
cursor: "pointer",
|
|
3398
|
+
fontSize: 13,
|
|
3399
|
+
color: "#0f172a"
|
|
3400
|
+
},
|
|
3401
|
+
children: [
|
|
3402
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
3403
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
3404
|
+
]
|
|
3405
|
+
},
|
|
3406
|
+
m.tenantId
|
|
3407
|
+
)) }) : null
|
|
3408
|
+
]
|
|
3409
|
+
}
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
function useImpersonation() {
|
|
3413
|
+
const { snapshot } = useCtx();
|
|
3414
|
+
return (0, import_react.useMemo)(() => {
|
|
3415
|
+
const claims = snapshot.claims;
|
|
3416
|
+
const isImpersonating = claims?.purpose === "impersonation" && !!claims?.act?.sub;
|
|
3417
|
+
return {
|
|
3418
|
+
isImpersonating,
|
|
3419
|
+
actor: isImpersonating ? claims.act : null,
|
|
3420
|
+
target: isImpersonating ? snapshot.user : null
|
|
3421
|
+
};
|
|
3422
|
+
}, [snapshot]);
|
|
3423
|
+
}
|
|
3424
|
+
function ImpersonationBanner({ render, onExit, className, style } = {}) {
|
|
3425
|
+
const t2 = useT();
|
|
3426
|
+
const info = useImpersonation();
|
|
3427
|
+
const { manager } = useCtx();
|
|
3428
|
+
const exit = (0, import_react.useCallback)(async () => {
|
|
3429
|
+
if (onExit) return void onExit();
|
|
3430
|
+
const { exitImpersonation: exitImpersonation2 } = await Promise.resolve().then(() => (init_reverify(), reverify_exports));
|
|
3431
|
+
const restored = exitImpersonation2(manager);
|
|
3432
|
+
if (restored) return;
|
|
3433
|
+
const { signOut: signOut2 } = await Promise.resolve().then(() => (init_signIn(), signIn_exports));
|
|
3434
|
+
await signOut2(manager);
|
|
3435
|
+
}, [manager, onExit]);
|
|
3436
|
+
if (!info.isImpersonating) return null;
|
|
3437
|
+
if (render) return (0, import_react.createElement)(import_react.Fragment, null, render({ ...info, exit }));
|
|
3438
|
+
const targetLabel = info.target?.email || info.target?.name || info.target?.sub || "user";
|
|
3439
|
+
const _actorLabel = info.actor?.email || info.actor?.name || info.actor?.sub || "admin";
|
|
3440
|
+
void _actorLabel;
|
|
3441
|
+
return (0, import_react.createElement)(
|
|
3442
|
+
"div",
|
|
3443
|
+
{
|
|
3444
|
+
role: "alert",
|
|
3445
|
+
className,
|
|
3446
|
+
style: {
|
|
3447
|
+
position: "sticky",
|
|
3448
|
+
top: 0,
|
|
3449
|
+
left: 0,
|
|
3450
|
+
right: 0,
|
|
3451
|
+
zIndex: 9999,
|
|
3452
|
+
background: "#b91c1c",
|
|
3453
|
+
color: "#fff",
|
|
3454
|
+
padding: "8px 16px",
|
|
3455
|
+
display: "flex",
|
|
3456
|
+
alignItems: "center",
|
|
3457
|
+
justifyContent: "space-between",
|
|
3458
|
+
fontSize: 13,
|
|
3459
|
+
fontFamily: "system-ui, sans-serif",
|
|
3460
|
+
...style
|
|
3461
|
+
}
|
|
3462
|
+
},
|
|
3463
|
+
(0, import_react.createElement)(
|
|
3464
|
+
"span",
|
|
3465
|
+
null,
|
|
3466
|
+
t2("impersonation.banner", { targetEmail: targetLabel })
|
|
3467
|
+
),
|
|
3468
|
+
(0, import_react.createElement)(
|
|
3469
|
+
"button",
|
|
3470
|
+
{
|
|
3471
|
+
type: "button",
|
|
3472
|
+
onClick: exit,
|
|
3473
|
+
style: {
|
|
3474
|
+
background: "rgba(255,255,255,0.18)",
|
|
3475
|
+
color: "#fff",
|
|
3476
|
+
border: "1px solid rgba(255,255,255,0.4)",
|
|
3477
|
+
borderRadius: 4,
|
|
3478
|
+
padding: "4px 10px",
|
|
3479
|
+
cursor: "pointer",
|
|
3480
|
+
fontSize: 12
|
|
3481
|
+
}
|
|
3482
|
+
},
|
|
3483
|
+
t2("impersonation.exit")
|
|
3484
|
+
)
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
function useReverification(fn, opts = {}) {
|
|
3488
|
+
const { manager } = useCtx();
|
|
3489
|
+
const level = opts.level ?? "password";
|
|
3490
|
+
return (async (...args) => {
|
|
3491
|
+
let token = null;
|
|
3492
|
+
let res = await fn(token)(...args);
|
|
3493
|
+
const code = await peekErrorCode(res);
|
|
3494
|
+
const isReverifyError = res.status === 401 && code && (code === "REVERIFICATION_REQUIRED" || code === "REVERIFICATION_EXPIRED" || code === "REVERIFICATION_INVALID" || code === "REVERIFICATION_USED" || code === "REVERIFICATION_LEVEL_INSUFFICIENT");
|
|
3495
|
+
if (!isReverifyError) return res;
|
|
3496
|
+
const prompt = opts.prompt ?? defaultReverifyPrompt;
|
|
3497
|
+
const credentials = await prompt(level);
|
|
3498
|
+
if (!credentials) {
|
|
3499
|
+
throw new Error("Reverification cancelled");
|
|
3500
|
+
}
|
|
3501
|
+
const { reverify: reverify2 } = await Promise.resolve().then(() => (init_reverify(), reverify_exports));
|
|
3502
|
+
const minted = await reverify2(manager, { level, ...credentials });
|
|
3503
|
+
token = minted.token;
|
|
3504
|
+
res = await fn(token)(...args);
|
|
3505
|
+
return res;
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
async function peekErrorCode(res) {
|
|
3509
|
+
try {
|
|
3510
|
+
const cloned = res.clone();
|
|
3511
|
+
const body = await cloned.json();
|
|
3512
|
+
return body?.error?.code ?? null;
|
|
3513
|
+
} catch {
|
|
3514
|
+
return null;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
async function defaultReverifyPrompt(level) {
|
|
3518
|
+
if (typeof window === "undefined") return null;
|
|
3519
|
+
if (level === "password") {
|
|
3520
|
+
const password = window.prompt("Confirm your password to continue");
|
|
3521
|
+
if (!password) return null;
|
|
3522
|
+
return { password };
|
|
3523
|
+
}
|
|
3524
|
+
const totp = window.prompt("Enter your MFA code to continue");
|
|
3525
|
+
if (!totp) return null;
|
|
3526
|
+
return { totp, method: "totp" };
|
|
3527
|
+
}
|
|
3528
|
+
function slugify(input) {
|
|
3529
|
+
return input.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
3530
|
+
}
|
|
3531
|
+
function CreateOrganization({ iqAuthBaseUrl, onCreated, redirectUrl, unstyled, appearance, className }) {
|
|
3532
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3533
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
3534
|
+
const [slug, setSlug] = (0, import_react.useState)("");
|
|
3535
|
+
const [slugTouched, setSlugTouched] = (0, import_react.useState)(false);
|
|
3536
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
3537
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
3538
|
+
const [created, setCreated] = (0, import_react.useState)(null);
|
|
3539
|
+
const [slugCheck, setSlugCheck] = (0, import_react.useState)({ status: "idle" });
|
|
3540
|
+
(0, import_react.useEffect)(() => {
|
|
3541
|
+
if (!slugTouched) setSlug(slugify(name));
|
|
3542
|
+
}, [name, slugTouched]);
|
|
3543
|
+
(0, import_react.useEffect)(() => {
|
|
3544
|
+
const s = slug.trim().toLowerCase();
|
|
3545
|
+
if (!s) {
|
|
3546
|
+
setSlugCheck({ status: "idle" });
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
if (!/^[a-z0-9-]{2,64}$/.test(s)) {
|
|
3550
|
+
setSlugCheck({ status: "invalid", checked: s });
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
setSlugCheck({ status: "checking", checked: s });
|
|
3554
|
+
const handle = setTimeout(async () => {
|
|
3555
|
+
try {
|
|
3556
|
+
const res = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/check-slug?slug=${encodeURIComponent(s)}`, {
|
|
3557
|
+
credentials: "include",
|
|
3558
|
+
headers: { Accept: "application/json" }
|
|
3559
|
+
});
|
|
3560
|
+
const body = await res.json().catch(() => ({}));
|
|
3561
|
+
const data = body.data;
|
|
3562
|
+
if (!data) {
|
|
3563
|
+
setSlugCheck({ status: "idle", checked: s });
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
if (data.reason === "INVALID_FORMAT") {
|
|
3567
|
+
setSlugCheck({ status: "invalid", checked: s });
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
setSlugCheck({ status: data.available ? "available" : "taken", checked: s });
|
|
3571
|
+
} catch {
|
|
3572
|
+
setSlugCheck({ status: "idle", checked: s });
|
|
3573
|
+
}
|
|
3574
|
+
}, 350);
|
|
3575
|
+
return () => clearTimeout(handle);
|
|
3576
|
+
}, [slug, iqAuthBaseUrl]);
|
|
3577
|
+
const submit = async (e) => {
|
|
3578
|
+
e.preventDefault();
|
|
3579
|
+
setSubmitting(true);
|
|
3580
|
+
setError(null);
|
|
3581
|
+
try {
|
|
3582
|
+
const res = await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants`, {
|
|
3583
|
+
method: "POST",
|
|
3584
|
+
headers: { "Content-Type": "application/json" },
|
|
3585
|
+
credentials: "include",
|
|
3586
|
+
body: JSON.stringify({ name: name.trim(), slug: slug.trim() })
|
|
3587
|
+
});
|
|
3588
|
+
const payload = res;
|
|
3589
|
+
const tenant = payload.data ?? payload;
|
|
3590
|
+
const next = { id: tenant.id, name: tenant.name, slug: tenant.slug };
|
|
3591
|
+
setCreated(next);
|
|
3592
|
+
onCreated?.(next);
|
|
3593
|
+
setName("");
|
|
3594
|
+
setSlug("");
|
|
3595
|
+
setSlugTouched(false);
|
|
3596
|
+
if (redirectUrl && typeof window !== "undefined") {
|
|
3597
|
+
const url2 = typeof redirectUrl === "function" ? redirectUrl(next) : redirectUrl;
|
|
3598
|
+
if (url2) window.location.assign(url2);
|
|
3599
|
+
}
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
setError(err instanceof Error ? err.message : "Failed to create organization");
|
|
3602
|
+
} finally {
|
|
3603
|
+
setSubmitting(false);
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
const form = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, "data-iqauth-sdk-create-org": "", "aria-labelledby": "iqauth-create-org-heading", children: [
|
|
3607
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
3608
|
+
created ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { role: "status", style: { fontSize: 13, color: "#047857" }, children: [
|
|
3609
|
+
"Organization \u201C",
|
|
3610
|
+
created.name,
|
|
3611
|
+
"\u201D created."
|
|
3612
|
+
] }) : null,
|
|
3613
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Organization name", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-create-org-name", autoFocus: true, style: inputStyle(), value: name, onChange: (e) => setName(e.target.value), required: true, minLength: 2, "aria-required": "true" }) }),
|
|
3614
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { label: "Organization slug", children: [
|
|
3615
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3616
|
+
"input",
|
|
3617
|
+
{
|
|
3618
|
+
"data-testid": "input-create-org-slug",
|
|
3619
|
+
style: { ...inputStyle(), fontFamily: "monospace" },
|
|
3620
|
+
value: slug,
|
|
3621
|
+
onChange: (e) => {
|
|
3622
|
+
setSlugTouched(true);
|
|
3623
|
+
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"));
|
|
3624
|
+
},
|
|
3625
|
+
required: true,
|
|
3626
|
+
pattern: "[a-z0-9-]+",
|
|
3627
|
+
minLength: 2,
|
|
3628
|
+
"aria-required": "true",
|
|
3629
|
+
"aria-describedby": "iqauth-create-org-slug-hint",
|
|
3630
|
+
"aria-invalid": slugCheck.status === "taken" || slugCheck.status === "invalid"
|
|
3631
|
+
}
|
|
3632
|
+
),
|
|
3633
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3634
|
+
"p",
|
|
3635
|
+
{
|
|
3636
|
+
id: "iqauth-create-org-slug-hint",
|
|
3637
|
+
"data-testid": "text-create-org-slug-status",
|
|
3638
|
+
role: "status",
|
|
3639
|
+
"aria-live": "polite",
|
|
3640
|
+
style: { fontSize: 12, marginTop: 4, color: slugCheck.status === "taken" || slugCheck.status === "invalid" ? "#b91c1c" : slugCheck.status === "available" ? "#047857" : "inherit", opacity: slugCheck.status === "checking" ? 0.6 : 1 },
|
|
3641
|
+
children: slugCheck.status === "checking" ? "Checking availability\u2026" : slugCheck.status === "available" ? "Slug is available." : slugCheck.status === "taken" ? "That slug is taken." : slugCheck.status === "invalid" ? "Slugs must be 2\u201364 chars, lowercase letters/numbers/hyphens." : "We'll auto-generate a slug from the name; you can override it."
|
|
3642
|
+
}
|
|
3643
|
+
)
|
|
3644
|
+
] }),
|
|
3645
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3646
|
+
PrimaryButton,
|
|
3647
|
+
{
|
|
3648
|
+
"data-testid": "button-create-org-submit",
|
|
3649
|
+
type: "submit",
|
|
3650
|
+
disabled: submitting || !name.trim() || !slug.trim() || slugCheck.status === "taken" || slugCheck.status === "invalid" || slugCheck.status === "checking",
|
|
3651
|
+
children: submitting ? "Creating\u2026" : "Create organization"
|
|
3652
|
+
}
|
|
3653
|
+
)
|
|
3654
|
+
] });
|
|
3655
|
+
if (unstyled) {
|
|
3656
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, "data-iqauth-sdk-create-org-bare": "", children: [
|
|
3657
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-create-org-heading", style: { fontSize: 14, fontWeight: 600, margin: "0 0 12px" }, children: "Create organization" }),
|
|
3658
|
+
form
|
|
3659
|
+
] });
|
|
3660
|
+
}
|
|
3661
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: "Create organization", subtitle: "Spin up a new tenant for this app.", children: form });
|
|
3662
|
+
}
|
|
3663
|
+
function OrganizationProfile({ iqAuthBaseUrl, tenantId: tenantIdProp, tabs, onDeleted, appearance, className }) {
|
|
3664
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3665
|
+
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
3666
|
+
const visibleTabs = tabs && tabs.length > 0 ? tabs : ["general", "members", "invitations", "danger"];
|
|
3667
|
+
const [activeTab, setActiveTab] = (0, import_react.useState)(visibleTabs[0]);
|
|
3668
|
+
const [tenantId, setTenantId] = (0, import_react.useState)(tenantIdProp || null);
|
|
3669
|
+
const [tenant, setTenant] = (0, import_react.useState)(null);
|
|
3670
|
+
const [members, setMembers] = (0, import_react.useState)([]);
|
|
3671
|
+
const [pendingInvites, setPendingInvites] = (0, import_react.useState)([]);
|
|
3672
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
3673
|
+
const [invitesLoading, setInvitesLoading] = (0, import_react.useState)(false);
|
|
3674
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
3675
|
+
const [renameValue, setRenameValue] = (0, import_react.useState)("");
|
|
3676
|
+
const [slugValue, setSlugValue] = (0, import_react.useState)("");
|
|
3677
|
+
const [renameSubmitting, setRenameSubmitting] = (0, import_react.useState)(false);
|
|
3678
|
+
const [inviteEmail, setInviteEmail] = (0, import_react.useState)("");
|
|
3679
|
+
const [inviteRole, setInviteRole] = (0, import_react.useState)("tenant_member");
|
|
3680
|
+
const [inviteSubmitting, setInviteSubmitting] = (0, import_react.useState)(false);
|
|
3681
|
+
const [actionMessage, setActionMessage] = (0, import_react.useState)(null);
|
|
3682
|
+
const [confirmDeleteText, setConfirmDeleteText] = (0, import_react.useState)("");
|
|
3683
|
+
const [confirmDeletePassword, setConfirmDeletePassword] = (0, import_react.useState)("");
|
|
3684
|
+
const [deleteSubmitting, setDeleteSubmitting] = (0, import_react.useState)(false);
|
|
3685
|
+
const { user } = useUser();
|
|
3686
|
+
const callerRole = user?.role || null;
|
|
3687
|
+
const callerIsAdmin = callerRole === "tenant_admin" || callerRole === "platform_admin";
|
|
3688
|
+
const visibleTabsFiltered = visibleTabs.filter((t2) => t2 !== "danger" || callerIsAdmin);
|
|
3689
|
+
(0, import_react.useEffect)(() => {
|
|
3690
|
+
let cancelled = false;
|
|
3691
|
+
(async () => {
|
|
3692
|
+
try {
|
|
3693
|
+
let tid = tenantIdProp || null;
|
|
3694
|
+
if (!tid) {
|
|
3695
|
+
const me = await fetch(`${baseUrl}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json());
|
|
3696
|
+
tid = me?.data?.tenantId || null;
|
|
3697
|
+
}
|
|
3698
|
+
if (!tid) {
|
|
3699
|
+
if (!cancelled) {
|
|
3700
|
+
setError("No active tenant");
|
|
3701
|
+
setLoading(false);
|
|
3702
|
+
}
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
const t2 = await fetch(`${baseUrl}/api/v1/tenants/${tid}`, { credentials: "include" }).then((r) => r.json());
|
|
3706
|
+
const m = await fetch(`${baseUrl}/api/v1/tenants/${tid}/users`, { credentials: "include" }).then((r) => r.json());
|
|
3707
|
+
if (cancelled) return;
|
|
3708
|
+
setTenantId(tid);
|
|
3709
|
+
setTenant(t2?.data ? { id: t2.data.id, name: t2.data.name, slug: t2.data.slug } : null);
|
|
3710
|
+
setRenameValue(t2?.data?.name || "");
|
|
3711
|
+
setSlugValue(t2?.data?.slug || "");
|
|
3712
|
+
const rows = m?.data || [];
|
|
3713
|
+
setMembers(rows.map((row) => ({
|
|
3714
|
+
userId: String(row.userId ?? row.user?.id ?? row.id ?? ""),
|
|
3715
|
+
email: String(row.email ?? row.user?.email ?? ""),
|
|
3716
|
+
name: String(row.name ?? row.user?.name ?? ""),
|
|
3717
|
+
role: String(row.role ?? row.roles?.[0] ?? "tenant_member"),
|
|
3718
|
+
joinedAt: row.joinedAt ?? row.createdAt
|
|
3719
|
+
})));
|
|
3720
|
+
} catch (err) {
|
|
3721
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load organization");
|
|
3722
|
+
} finally {
|
|
3723
|
+
if (!cancelled) setLoading(false);
|
|
3724
|
+
}
|
|
3725
|
+
})();
|
|
3726
|
+
return () => {
|
|
3727
|
+
cancelled = true;
|
|
3728
|
+
};
|
|
3729
|
+
}, [baseUrl, tenantIdProp]);
|
|
3730
|
+
const loadInvites = async (tid) => {
|
|
3731
|
+
setInvitesLoading(true);
|
|
3732
|
+
try {
|
|
3733
|
+
const r = await fetch(`${baseUrl}/api/v1/invites?tenantId=${encodeURIComponent(tid)}&status=pending`, { credentials: "include" }).then((res) => res.json());
|
|
3734
|
+
const rows = r?.data || [];
|
|
3735
|
+
setPendingInvites(rows.map((inv) => ({
|
|
3736
|
+
id: String(inv.id),
|
|
3737
|
+
email: String(inv.email),
|
|
3738
|
+
role: String(inv.role),
|
|
3739
|
+
status: String(inv.status),
|
|
3740
|
+
expiresAt: inv.expiresAt,
|
|
3741
|
+
createdAt: inv.createdAt
|
|
3742
|
+
})));
|
|
3743
|
+
} catch (err) {
|
|
3744
|
+
setError(err instanceof Error ? err.message : "Failed to load invitations");
|
|
3745
|
+
} finally {
|
|
3746
|
+
setInvitesLoading(false);
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
1775
3749
|
(0, import_react.useEffect)(() => {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
fetch(`${
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
3750
|
+
if (activeTab === "invitations" && tenantId) loadInvites(tenantId);
|
|
3751
|
+
}, [activeTab, tenantId]);
|
|
3752
|
+
const reloadMembers = async () => {
|
|
3753
|
+
if (!tenantId) return;
|
|
3754
|
+
const m = await fetch(`${baseUrl}/api/v1/tenants/${tenantId}/users`, { credentials: "include" }).then((r) => r.json());
|
|
3755
|
+
const rows = m?.data || [];
|
|
3756
|
+
setMembers(rows.map((row) => ({
|
|
3757
|
+
userId: String(row.userId ?? row.user?.id ?? row.id ?? ""),
|
|
3758
|
+
email: String(row.email ?? row.user?.email ?? ""),
|
|
3759
|
+
name: String(row.name ?? row.user?.name ?? ""),
|
|
3760
|
+
role: String(row.role ?? row.roles?.[0] ?? "tenant_member"),
|
|
3761
|
+
joinedAt: row.joinedAt ?? row.createdAt
|
|
3762
|
+
})));
|
|
3763
|
+
};
|
|
3764
|
+
const submitRename = async (e) => {
|
|
1784
3765
|
e.preventDefault();
|
|
1785
|
-
|
|
3766
|
+
if (!tenantId) return;
|
|
3767
|
+
setRenameSubmitting(true);
|
|
3768
|
+
setError(null);
|
|
1786
3769
|
try {
|
|
1787
|
-
|
|
3770
|
+
const body = {};
|
|
3771
|
+
if (renameValue.trim() && renameValue.trim() !== tenant?.name) body.name = renameValue.trim();
|
|
3772
|
+
if (slugValue.trim() && slugValue.trim() !== tenant?.slug) body.slug = slugValue.trim();
|
|
3773
|
+
if (Object.keys(body).length === 0) {
|
|
3774
|
+
setRenameSubmitting(false);
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3777
|
+
const res = await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}`, {
|
|
3778
|
+
method: "PATCH",
|
|
3779
|
+
headers: { "Content-Type": "application/json" },
|
|
3780
|
+
credentials: "include",
|
|
3781
|
+
body: JSON.stringify(body)
|
|
3782
|
+
});
|
|
3783
|
+
const t2 = res?.data ?? res;
|
|
3784
|
+
setTenant({ id: t2.id, name: t2.name, slug: t2.slug });
|
|
3785
|
+
setActionMessage("Organization saved.");
|
|
3786
|
+
} catch (err) {
|
|
3787
|
+
setError(err instanceof Error ? err.message : "Failed to save");
|
|
3788
|
+
} finally {
|
|
3789
|
+
setRenameSubmitting(false);
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
const submitInvite = async (e) => {
|
|
3793
|
+
e.preventDefault();
|
|
3794
|
+
if (!tenantId) return;
|
|
3795
|
+
setInviteSubmitting(true);
|
|
3796
|
+
setError(null);
|
|
3797
|
+
try {
|
|
3798
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/invite`, {
|
|
1788
3799
|
method: "POST",
|
|
1789
3800
|
headers: { "Content-Type": "application/json" },
|
|
1790
|
-
|
|
3801
|
+
credentials: "include",
|
|
3802
|
+
body: JSON.stringify({ email: inviteEmail.trim(), role: inviteRole })
|
|
1791
3803
|
});
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
3804
|
+
setActionMessage(`Invitation sent to ${inviteEmail.trim()}.`);
|
|
3805
|
+
setInviteEmail("");
|
|
3806
|
+
if (activeTab === "invitations") await loadInvites(tenantId);
|
|
1795
3807
|
} catch (err) {
|
|
1796
|
-
|
|
3808
|
+
setError(err instanceof Error ? err.message : "Failed to invite");
|
|
3809
|
+
} finally {
|
|
3810
|
+
setInviteSubmitting(false);
|
|
1797
3811
|
}
|
|
1798
3812
|
};
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
3813
|
+
const changeRole = async (userId, role) => {
|
|
3814
|
+
if (!tenantId) return;
|
|
3815
|
+
try {
|
|
3816
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/${userId}/role`, {
|
|
3817
|
+
method: "PATCH",
|
|
3818
|
+
headers: { "Content-Type": "application/json" },
|
|
3819
|
+
credentials: "include",
|
|
3820
|
+
body: JSON.stringify({ role })
|
|
3821
|
+
});
|
|
3822
|
+
await reloadMembers();
|
|
3823
|
+
setActionMessage("Role updated.");
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
setError(err instanceof Error ? err.message : "Failed to update role");
|
|
3826
|
+
}
|
|
1802
3827
|
};
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
3828
|
+
const removeMember = async (userId) => {
|
|
3829
|
+
if (!tenantId) return;
|
|
3830
|
+
try {
|
|
3831
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/${userId}`, { method: "DELETE", credentials: "include" });
|
|
3832
|
+
await reloadMembers();
|
|
3833
|
+
setActionMessage("Member removed.");
|
|
3834
|
+
} catch (err) {
|
|
3835
|
+
setError(err instanceof Error ? err.message : "Failed to remove member");
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
const resendInvite = async (id) => {
|
|
3839
|
+
if (!tenantId) return;
|
|
3840
|
+
try {
|
|
3841
|
+
await jsonFetch(`${baseUrl}/api/v1/invites/${id}/resend`, { method: "POST", credentials: "include" });
|
|
3842
|
+
setActionMessage("Invitation resent.");
|
|
3843
|
+
await loadInvites(tenantId);
|
|
3844
|
+
} catch (err) {
|
|
3845
|
+
setError(err instanceof Error ? err.message : "Failed to resend invitation");
|
|
3846
|
+
}
|
|
3847
|
+
};
|
|
3848
|
+
const revokeInvite = async (id) => {
|
|
3849
|
+
if (!tenantId) return;
|
|
3850
|
+
try {
|
|
3851
|
+
await jsonFetch(`${baseUrl}/api/v1/invites/${id}/revoke`, { method: "POST", credentials: "include" });
|
|
3852
|
+
setActionMessage("Invitation revoked.");
|
|
3853
|
+
await loadInvites(tenantId);
|
|
3854
|
+
} catch (err) {
|
|
3855
|
+
setError(err instanceof Error ? err.message : "Failed to revoke invitation");
|
|
3856
|
+
}
|
|
3857
|
+
};
|
|
3858
|
+
const submitDelete = async () => {
|
|
3859
|
+
if (!tenantId || !tenant) return;
|
|
3860
|
+
if (confirmDeleteText !== tenant.slug) {
|
|
3861
|
+
setError("Type the organization slug to confirm deletion.");
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
if (!confirmDeletePassword) {
|
|
3865
|
+
setError("Re-enter your password to confirm deletion.");
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
setDeleteSubmitting(true);
|
|
3869
|
+
setError(null);
|
|
3870
|
+
try {
|
|
3871
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}`, {
|
|
3872
|
+
method: "DELETE",
|
|
3873
|
+
credentials: "include",
|
|
3874
|
+
headers: { "Content-Type": "application/json" },
|
|
3875
|
+
body: JSON.stringify({ confirmPassword: confirmDeletePassword })
|
|
3876
|
+
});
|
|
3877
|
+
setActionMessage("Organization deleted.");
|
|
3878
|
+
setConfirmDeletePassword("");
|
|
3879
|
+
onDeleted?.(tenantId);
|
|
3880
|
+
} catch (err) {
|
|
3881
|
+
setError(err instanceof Error ? err.message : "Failed to delete organization");
|
|
3882
|
+
} finally {
|
|
3883
|
+
setDeleteSubmitting(false);
|
|
3884
|
+
}
|
|
3885
|
+
};
|
|
3886
|
+
if (loading) {
|
|
3887
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: "Organization", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", "aria-live": "polite", style: { fontSize: 13 }, children: "Loading\u2026" }) });
|
|
3888
|
+
}
|
|
3889
|
+
const tabBtnStyle = (key) => ({
|
|
3890
|
+
background: activeTab === key ? `${branding?.accentColor || "#6366f1"}1a` : "transparent",
|
|
3891
|
+
border: `1px solid ${activeTab === key ? branding?.accentColor || "#6366f1" : "rgba(15,23,42,0.12)"}`,
|
|
3892
|
+
color: branding?.primaryColor || "#0f172a",
|
|
3893
|
+
padding: "6px 12px",
|
|
3894
|
+
borderRadius: 6,
|
|
3895
|
+
cursor: "pointer",
|
|
3896
|
+
fontSize: 12,
|
|
3897
|
+
fontWeight: 500
|
|
3898
|
+
});
|
|
3899
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: tenant?.name || "Organization", subtitle: tenant?.slug ? `slug: ${tenant.slug}` : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, "data-iqauth-sdk-org-profile": "", children: [
|
|
3900
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
3901
|
+
actionMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", "aria-live": "polite", style: { fontSize: 13, color: "#047857", margin: 0 }, children: actionMessage }) : null,
|
|
3902
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "tablist", "aria-label": "Organization sections", style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: visibleTabsFiltered.map((t2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3903
|
+
"button",
|
|
3904
|
+
{
|
|
3905
|
+
role: "tab",
|
|
3906
|
+
"aria-selected": activeTab === t2,
|
|
3907
|
+
"aria-controls": `iqauth-org-tab-${t2}`,
|
|
3908
|
+
"data-testid": `tab-org-${t2}`,
|
|
3909
|
+
type: "button",
|
|
3910
|
+
onClick: () => setActiveTab(t2),
|
|
3911
|
+
style: tabBtnStyle(t2),
|
|
3912
|
+
children: t2 === "general" ? "General" : t2 === "members" ? `Members (${members.length})` : t2 === "invitations" ? "Invitations" : "Danger zone"
|
|
3913
|
+
},
|
|
3914
|
+
t2
|
|
3915
|
+
)) }),
|
|
3916
|
+
activeTab === "general" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { role: "tabpanel", id: "iqauth-org-tab-general", "aria-labelledby": "tab-org-general", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
3917
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: "Settings" }),
|
|
3918
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitRename, style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
3919
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Organization name", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-org-rename", style: inputStyle(), value: renameValue, onChange: (e) => setRenameValue(e.target.value), required: true, minLength: 2 }) }),
|
|
3920
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Slug", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-org-slug", style: { ...inputStyle(), fontFamily: "monospace" }, value: slugValue, onChange: (e) => setSlugValue(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-")), required: true, pattern: "[a-z0-9-]+", minLength: 2 }) }),
|
|
3921
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { "data-testid": "button-org-rename", type: "submit", disabled: renameSubmitting || renameValue.trim() === tenant?.name && slugValue.trim() === tenant?.slug, children: renameSubmitting ? "Saving\u2026" : "Save changes" })
|
|
1827
3922
|
] })
|
|
1828
|
-
] }),
|
|
1829
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "
|
|
1830
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1831
|
-
|
|
1832
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
3923
|
+
] }) : null,
|
|
3924
|
+
activeTab === "members" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { role: "tabpanel", id: "iqauth-org-tab-members", "aria-labelledby": "tab-org-members", style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
3925
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitInvite, style: { display: "flex", gap: 8, flexWrap: "wrap" }, "aria-label": "Invite a new member", children: [
|
|
3926
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-org-invite-email", type: "email", placeholder: "email@company.com", "aria-label": "Email", style: { ...inputStyle(), flex: 1, minWidth: 180 }, value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), required: true }),
|
|
3927
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { "data-testid": "select-org-invite-role", "aria-label": "Role", style: { ...inputStyle(), width: "auto" }, value: inviteRole, onChange: (e) => setInviteRole(e.target.value), children: [
|
|
3928
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_member", children: "tenant_member" }),
|
|
3929
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_admin", children: "tenant_admin" })
|
|
3930
|
+
] }),
|
|
3931
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { "data-testid": "button-org-invite", type: "submit", disabled: inviteSubmitting || !inviteEmail.trim(), children: inviteSubmitting ? "Sending\u2026" : "Send invite" })
|
|
3932
|
+
] }),
|
|
3933
|
+
members.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6 }, children: "No members yet." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { "aria-label": "Members", style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: members.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { "data-testid": `row-org-member-${m.userId}`, style: { display: "grid", gridTemplateColumns: "1fr auto auto", gap: 8, alignItems: "center", padding: "8px 10px", background: "rgba(15,23,42,0.04)", borderRadius: 6, fontSize: 13 }, children: [
|
|
3934
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
3935
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.name || m.email }),
|
|
3936
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.7 }, children: m.email })
|
|
3937
|
+
] }),
|
|
3938
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { "data-testid": `select-org-member-role-${m.userId}`, "aria-label": `Role for ${m.email}`, value: m.role, onChange: (e) => changeRole(m.userId, e.target.value), style: { ...inputStyle(), width: "auto", padding: "4px 8px", fontSize: 12 }, children: [
|
|
3939
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_member", children: "tenant_member" }),
|
|
3940
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_admin", children: "tenant_admin" })
|
|
3941
|
+
] }),
|
|
3942
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3943
|
+
"button",
|
|
3944
|
+
{
|
|
3945
|
+
type: "button",
|
|
3946
|
+
"data-testid": `button-org-member-remove-${m.userId}`,
|
|
3947
|
+
"aria-label": `Remove ${m.email}`,
|
|
3948
|
+
onClick: () => removeMember(m.userId),
|
|
3949
|
+
style: { background: "transparent", border: "1px solid rgba(220,38,38,0.4)", color: "#b91c1c", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
|
|
3950
|
+
children: "Remove"
|
|
3951
|
+
}
|
|
3952
|
+
)
|
|
3953
|
+
] }, m.userId)) })
|
|
3954
|
+
] }) : null,
|
|
3955
|
+
activeTab === "invitations" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { role: "tabpanel", id: "iqauth-org-tab-invitations", "aria-labelledby": "tab-org-invitations", style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
3956
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: "Pending invitations" }),
|
|
3957
|
+
invitesLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", "aria-live": "polite", style: { fontSize: 13 }, children: "Loading\u2026" }) : pendingInvites.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6 }, children: "No pending invitations." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { "aria-label": "Pending invitations", style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: pendingInvites.map((inv) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { "data-testid": `row-org-invite-${inv.id}`, style: { display: "grid", gridTemplateColumns: "1fr auto auto", gap: 8, alignItems: "center", padding: "8px 10px", background: "rgba(15,23,42,0.04)", borderRadius: 6, fontSize: 13 }, children: [
|
|
3958
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
3959
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: inv.email }),
|
|
3960
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, opacity: 0.7 }, children: [
|
|
3961
|
+
"role: ",
|
|
3962
|
+
inv.role,
|
|
3963
|
+
inv.expiresAt ? ` \u2022 expires ${new Date(inv.expiresAt).toLocaleDateString()}` : ""
|
|
3964
|
+
] })
|
|
3965
|
+
] }),
|
|
3966
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3967
|
+
"button",
|
|
3968
|
+
{
|
|
3969
|
+
type: "button",
|
|
3970
|
+
"data-testid": `button-org-invite-resend-${inv.id}`,
|
|
3971
|
+
onClick: () => resendInvite(inv.id),
|
|
3972
|
+
style: { background: "transparent", border: "1px solid rgba(15,23,42,0.18)", color: "#0f172a", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
|
|
3973
|
+
children: "Resend"
|
|
3974
|
+
}
|
|
3975
|
+
),
|
|
3976
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3977
|
+
"button",
|
|
3978
|
+
{
|
|
3979
|
+
type: "button",
|
|
3980
|
+
"data-testid": `button-org-invite-revoke-${inv.id}`,
|
|
3981
|
+
onClick: () => revokeInvite(inv.id),
|
|
3982
|
+
style: { background: "transparent", border: "1px solid rgba(220,38,38,0.4)", color: "#b91c1c", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
|
|
3983
|
+
children: "Revoke"
|
|
3984
|
+
}
|
|
3985
|
+
)
|
|
3986
|
+
] }, inv.id)) })
|
|
3987
|
+
] }) : null,
|
|
3988
|
+
activeTab === "danger" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { role: "tabpanel", id: "iqauth-org-tab-danger", "aria-labelledby": "tab-org-danger", style: { display: "flex", flexDirection: "column", gap: 10, border: "1px solid rgba(220,38,38,0.3)", padding: 12, borderRadius: 8, background: "rgba(220,38,38,0.04)" }, children: [
|
|
3989
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0, color: "#b91c1c" }, children: "Delete organization" }),
|
|
3990
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 12, opacity: 0.85, margin: 0 }, children: [
|
|
3991
|
+
"This permanently deletes ",
|
|
3992
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: tenant?.name }),
|
|
3993
|
+
" and all of its members, roles, and audit history. To confirm, type the slug ",
|
|
3994
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: tenant?.slug }),
|
|
3995
|
+
" below."
|
|
3996
|
+
] }),
|
|
3997
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Type the organization slug to confirm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-org-delete-confirm", autoComplete: "off", placeholder: tenant?.slug || "", "aria-label": "Type slug to confirm", style: { ...inputStyle(), fontFamily: "monospace" }, value: confirmDeleteText, onChange: (e) => setConfirmDeleteText(e.target.value) }) }),
|
|
3998
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Re-enter your password", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-org-delete-password", type: "password", autoComplete: "current-password", "aria-label": "Password to confirm deletion", style: inputStyle(), value: confirmDeletePassword, onChange: (e) => setConfirmDeletePassword(e.target.value) }) }),
|
|
3999
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4000
|
+
"button",
|
|
4001
|
+
{
|
|
4002
|
+
type: "button",
|
|
4003
|
+
"data-testid": "button-org-delete",
|
|
4004
|
+
onClick: submitDelete,
|
|
4005
|
+
disabled: deleteSubmitting || confirmDeleteText !== tenant?.slug || !confirmDeletePassword,
|
|
4006
|
+
style: { background: "#b91c1c", color: "#fff", border: "none", padding: "8px 14px", borderRadius: 6, cursor: confirmDeleteText === tenant?.slug && confirmDeletePassword ? "pointer" : "not-allowed", fontSize: 13, opacity: confirmDeleteText === tenant?.slug && confirmDeletePassword ? 1 : 0.5 },
|
|
4007
|
+
children: deleteSubmitting ? "Deleting\u2026" : "Permanently delete organization"
|
|
4008
|
+
}
|
|
4009
|
+
)
|
|
4010
|
+
] }) : null
|
|
4011
|
+
] }) });
|
|
1837
4012
|
}
|
|
1838
|
-
function
|
|
4013
|
+
function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRedirectUrl, appearance, className }) {
|
|
4014
|
+
const [showCreateForm, setShowCreateForm] = (0, import_react.useState)(false);
|
|
4015
|
+
const reloadList = () => {
|
|
4016
|
+
setShowCreateForm(false);
|
|
4017
|
+
if (typeof window !== "undefined") setTimeout(() => window.location.reload(), 50);
|
|
4018
|
+
};
|
|
1839
4019
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
4020
|
+
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
1840
4021
|
const accent = branding?.accentColor || "#6366f1";
|
|
1841
4022
|
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
1842
4023
|
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
1843
|
-
const [
|
|
4024
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
4025
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
1844
4026
|
(0, import_react.useEffect)(() => {
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
4027
|
+
let cancelled = false;
|
|
4028
|
+
Promise.all([
|
|
4029
|
+
fetch(`${baseUrl}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()),
|
|
4030
|
+
fetch(`${baseUrl}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json())
|
|
4031
|
+
]).then(([me, mems]) => {
|
|
4032
|
+
if (cancelled) return;
|
|
4033
|
+
setActiveTenantId(me?.data?.tenantId || null);
|
|
4034
|
+
setMemberships(mems?.data?.memberships || mems?.data || []);
|
|
4035
|
+
}).catch((err) => {
|
|
4036
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load organizations");
|
|
4037
|
+
}).finally(() => {
|
|
4038
|
+
if (!cancelled) setLoading(false);
|
|
1850
4039
|
});
|
|
1851
|
-
|
|
1852
|
-
|
|
4040
|
+
return () => {
|
|
4041
|
+
cancelled = true;
|
|
4042
|
+
};
|
|
4043
|
+
}, [baseUrl]);
|
|
4044
|
+
const select = async (tenantId) => {
|
|
4045
|
+
if (tenantId === activeTenantId) {
|
|
4046
|
+
onSelect?.(tenantId);
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
1853
4049
|
try {
|
|
1854
|
-
await jsonFetch(`${
|
|
4050
|
+
await jsonFetch(`${baseUrl}/api/v1/auth/select-tenant`, {
|
|
1855
4051
|
method: "POST",
|
|
1856
4052
|
headers: { "Content-Type": "application/json" },
|
|
4053
|
+
credentials: "include",
|
|
1857
4054
|
body: JSON.stringify({ tenantId })
|
|
1858
4055
|
});
|
|
1859
4056
|
setActiveTenantId(tenantId);
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
4057
|
+
onSelect?.(tenantId);
|
|
4058
|
+
} catch (err) {
|
|
4059
|
+
setError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
1863
4060
|
}
|
|
1864
4061
|
};
|
|
1865
|
-
|
|
4062
|
+
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: [
|
|
4063
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
4064
|
+
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) => {
|
|
4065
|
+
const active = m.tenantId === activeTenantId;
|
|
4066
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
4067
|
+
"button",
|
|
4068
|
+
{
|
|
4069
|
+
type: "button",
|
|
4070
|
+
"data-testid": `button-org-list-${m.tenantId}`,
|
|
4071
|
+
onClick: () => select(m.tenantId),
|
|
4072
|
+
style: {
|
|
4073
|
+
display: "block",
|
|
4074
|
+
width: "100%",
|
|
4075
|
+
textAlign: "left",
|
|
4076
|
+
padding: "10px 12px",
|
|
4077
|
+
borderRadius: 6,
|
|
4078
|
+
cursor: "pointer",
|
|
4079
|
+
fontSize: 13,
|
|
4080
|
+
background: active ? `${accent}1a` : "transparent",
|
|
4081
|
+
border: `1px solid ${active ? accent : "rgba(15,23,42,0.12)"}`,
|
|
4082
|
+
color: branding?.primaryColor || "#0f172a"
|
|
4083
|
+
},
|
|
4084
|
+
children: [
|
|
4085
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
4086
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, opacity: 0.7 }, children: [
|
|
4087
|
+
(m.roles || []).join(", ") || "\u2014",
|
|
4088
|
+
active ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { marginLeft: 8, color: accent, fontWeight: 600 }, children: "active" }) : null
|
|
4089
|
+
] })
|
|
4090
|
+
]
|
|
4091
|
+
}
|
|
4092
|
+
) }, m.tenantId);
|
|
4093
|
+
}) }),
|
|
4094
|
+
showCreate ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 12, paddingTop: 12, borderTop: "1px solid rgba(15,23,42,0.08)" }, children: showCreateForm ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
4095
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4096
|
+
CreateOrganization,
|
|
4097
|
+
{
|
|
4098
|
+
iqAuthBaseUrl,
|
|
4099
|
+
unstyled: true,
|
|
4100
|
+
appearance,
|
|
4101
|
+
onCreated: (t2) => {
|
|
4102
|
+
onSelect?.(t2.id);
|
|
4103
|
+
reloadList();
|
|
4104
|
+
},
|
|
4105
|
+
redirectUrl: createRedirectUrl
|
|
4106
|
+
}
|
|
4107
|
+
),
|
|
4108
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4109
|
+
"button",
|
|
4110
|
+
{
|
|
4111
|
+
type: "button",
|
|
4112
|
+
"data-testid": "button-org-list-create-cancel",
|
|
4113
|
+
onClick: () => setShowCreateForm(false),
|
|
4114
|
+
style: { marginTop: 8, background: "transparent", border: "none", color: branding?.accentColor || "#6366f1", cursor: "pointer", fontSize: 12, padding: 0 },
|
|
4115
|
+
children: "Cancel"
|
|
4116
|
+
}
|
|
4117
|
+
)
|
|
4118
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4119
|
+
"button",
|
|
4120
|
+
{
|
|
4121
|
+
type: "button",
|
|
4122
|
+
"data-testid": "button-org-list-create",
|
|
4123
|
+
onClick: () => setShowCreateForm(true),
|
|
4124
|
+
style: { background: "transparent", border: `1px dashed ${accent}`, color: branding?.primaryColor || "#0f172a", padding: "10px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13, width: "100%" },
|
|
4125
|
+
children: "+ Create new organization"
|
|
4126
|
+
}
|
|
4127
|
+
) }) : null
|
|
4128
|
+
] }) });
|
|
4129
|
+
}
|
|
4130
|
+
function Waitlist({ iqAuthBaseUrl, appKey, appId, title, subtitle, successMessage, appearance, className }) {
|
|
4131
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl, appId);
|
|
4132
|
+
const [email, setEmail] = (0, import_react.useState)("");
|
|
4133
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
4134
|
+
const [organizationName, setOrganizationName] = (0, import_react.useState)("");
|
|
4135
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
4136
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4137
|
+
const [submitted, setSubmitted] = (0, import_react.useState)(null);
|
|
4138
|
+
const submit = async (e) => {
|
|
4139
|
+
e.preventDefault();
|
|
4140
|
+
setSubmitting(true);
|
|
4141
|
+
setError(null);
|
|
4142
|
+
try {
|
|
4143
|
+
const res = await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/waitlist`, {
|
|
4144
|
+
method: "POST",
|
|
4145
|
+
headers: { "Content-Type": "application/json" },
|
|
4146
|
+
body: JSON.stringify({
|
|
4147
|
+
email: email.trim().toLowerCase(),
|
|
4148
|
+
name: name.trim() || void 0,
|
|
4149
|
+
organizationName: organizationName.trim() || void 0,
|
|
4150
|
+
appKey: appKey || void 0,
|
|
4151
|
+
appId: appId || void 0
|
|
4152
|
+
})
|
|
4153
|
+
});
|
|
4154
|
+
const data = res?.data ?? res;
|
|
4155
|
+
setSubmitted({ duplicate: !!data?.duplicate });
|
|
4156
|
+
} catch (err) {
|
|
4157
|
+
setError(err instanceof Error ? err.message : "Failed to join the waitlist");
|
|
4158
|
+
} finally {
|
|
4159
|
+
setSubmitting(false);
|
|
4160
|
+
}
|
|
4161
|
+
};
|
|
4162
|
+
if (submitted) {
|
|
4163
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: title || "You\u2019re on the list", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-waitlist-success", style: { fontSize: 14 }, children: successMessage || (submitted.duplicate ? "You were already on the waitlist \u2014 we\u2019ll be in touch." : "Thanks! We\u2019ll email you when access opens up.") }) });
|
|
4164
|
+
}
|
|
4165
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { appearance, branding, className, title: title || "Join the waitlist", subtitle: subtitle || "Enter your email and we\u2019ll be in touch when access opens up.", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, "data-iqauth-sdk-waitlist": "", children: [
|
|
4166
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
4167
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Work email", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-waitlist-email", type: "email", autoComplete: "email", required: true, style: inputStyle(), value: email, onChange: (e) => setEmail(e.target.value) }) }),
|
|
4168
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Name (optional)", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-waitlist-name", autoComplete: "name", style: inputStyle(), value: name, onChange: (e) => setName(e.target.value) }) }),
|
|
4169
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Organization (optional)", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "data-testid": "input-waitlist-org", autoComplete: "organization", style: inputStyle(), value: organizationName, onChange: (e) => setOrganizationName(e.target.value) }) }),
|
|
4170
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { "data-testid": "button-waitlist-submit", type: "submit", disabled: submitting || !email, children: submitting ? "Joining\u2026" : "Join the waitlist" })
|
|
4171
|
+
] }) });
|
|
4172
|
+
}
|
|
4173
|
+
function usePasswordlessOptions(override) {
|
|
4174
|
+
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
4175
|
+
const baseFromCtx = ctx?.manager?.issuerUrl;
|
|
4176
|
+
const iqAuthBaseUrl = override?.iqAuthBaseUrl || baseFromCtx || (typeof window !== "undefined" ? window.location.origin : "");
|
|
4177
|
+
return { iqAuthBaseUrl, cookieSession: override?.cookieSession ?? true };
|
|
4178
|
+
}
|
|
4179
|
+
function useMagicLink(override) {
|
|
4180
|
+
const opts = usePasswordlessOptions(override);
|
|
4181
|
+
const [sent, setSent] = (0, import_react.useState)(false);
|
|
4182
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
4183
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4184
|
+
const request = (0, import_react.useCallback)(async (input) => {
|
|
4185
|
+
setBusy(true);
|
|
4186
|
+
setError(null);
|
|
4187
|
+
setSent(false);
|
|
4188
|
+
try {
|
|
4189
|
+
await requestMagicLink(opts, input);
|
|
4190
|
+
setSent(true);
|
|
4191
|
+
} catch (e) {
|
|
4192
|
+
setError(e instanceof Error ? e.message : "Magic link request failed");
|
|
4193
|
+
} finally {
|
|
4194
|
+
setBusy(false);
|
|
4195
|
+
}
|
|
4196
|
+
}, [opts.iqAuthBaseUrl, opts.cookieSession]);
|
|
4197
|
+
return { request, sent, busy, error };
|
|
4198
|
+
}
|
|
4199
|
+
function usePasskey(override) {
|
|
4200
|
+
const opts = usePasswordlessOptions(override);
|
|
4201
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
4202
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4203
|
+
const signIn2 = (0, import_react.useCallback)(async (input = {}) => {
|
|
4204
|
+
setBusy(true);
|
|
4205
|
+
setError(null);
|
|
4206
|
+
try {
|
|
4207
|
+
return await signInWithPasskey(opts, input);
|
|
4208
|
+
} catch (e) {
|
|
4209
|
+
const msg = e instanceof Error ? e.message : "Passkey sign-in failed";
|
|
4210
|
+
setError(msg);
|
|
4211
|
+
throw e;
|
|
4212
|
+
} finally {
|
|
4213
|
+
setBusy(false);
|
|
4214
|
+
}
|
|
4215
|
+
}, [opts.iqAuthBaseUrl]);
|
|
4216
|
+
const enroll = (0, import_react.useCallback)(async (name) => {
|
|
4217
|
+
setBusy(true);
|
|
4218
|
+
setError(null);
|
|
4219
|
+
try {
|
|
4220
|
+
return await enrollPasskey(opts, name);
|
|
4221
|
+
} catch (e) {
|
|
4222
|
+
const msg = e instanceof Error ? e.message : "Passkey enrollment failed";
|
|
4223
|
+
setError(msg);
|
|
4224
|
+
throw e;
|
|
4225
|
+
} finally {
|
|
4226
|
+
setBusy(false);
|
|
4227
|
+
}
|
|
4228
|
+
}, [opts.iqAuthBaseUrl]);
|
|
4229
|
+
return { signIn: signIn2, enroll, busy, error };
|
|
4230
|
+
}
|
|
4231
|
+
function useLinkedIdentities(override) {
|
|
4232
|
+
const opts = usePasswordlessOptions(override);
|
|
4233
|
+
const [identities, setIdentities] = (0, import_react.useState)([]);
|
|
4234
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
4235
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4236
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
4237
|
+
setLoading(true);
|
|
4238
|
+
setError(null);
|
|
4239
|
+
try {
|
|
4240
|
+
setIdentities(await listLinkedIdentities(opts));
|
|
4241
|
+
} catch (e) {
|
|
4242
|
+
setError(e instanceof Error ? e.message : "Failed to load identities");
|
|
4243
|
+
} finally {
|
|
4244
|
+
setLoading(false);
|
|
4245
|
+
}
|
|
4246
|
+
}, [opts.iqAuthBaseUrl]);
|
|
4247
|
+
const link = (0, import_react.useCallback)(async (input) => {
|
|
4248
|
+
await linkProvider(opts, input);
|
|
4249
|
+
await refresh();
|
|
4250
|
+
}, [opts.iqAuthBaseUrl, refresh]);
|
|
4251
|
+
const unlink = (0, import_react.useCallback)(async (provider, password) => {
|
|
4252
|
+
await unlinkProvider(opts, { provider, reauth: { password } });
|
|
4253
|
+
await refresh();
|
|
4254
|
+
}, [opts.iqAuthBaseUrl, refresh]);
|
|
4255
|
+
(0, import_react.useEffect)(() => {
|
|
4256
|
+
refresh();
|
|
4257
|
+
}, [refresh]);
|
|
4258
|
+
return { identities, loading, error, refresh, link, unlink };
|
|
4259
|
+
}
|
|
4260
|
+
function MagicLinkSignInForm(props) {
|
|
4261
|
+
const { request, sent, busy, error } = useMagicLink(props);
|
|
4262
|
+
const [email, setEmail] = (0, import_react.useState)("");
|
|
1866
4263
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1867
|
-
"
|
|
4264
|
+
"form",
|
|
1868
4265
|
{
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
4266
|
+
"data-testid": "form-magic-link",
|
|
4267
|
+
className: props.className,
|
|
4268
|
+
onSubmit: (e) => {
|
|
4269
|
+
e.preventDefault();
|
|
4270
|
+
if (email) void request({ email, appId: props.appId, redirectUri: props.redirectUri });
|
|
4271
|
+
},
|
|
4272
|
+
style: { display: "flex", flexDirection: "column", gap: 8 },
|
|
1873
4273
|
children: [
|
|
1874
4274
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1875
|
-
"
|
|
4275
|
+
"input",
|
|
1876
4276
|
{
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
4277
|
+
"data-testid": "input-magic-link-email",
|
|
4278
|
+
type: "email",
|
|
4279
|
+
required: true,
|
|
4280
|
+
value: email,
|
|
4281
|
+
placeholder: props.placeholder ?? "you@example.com",
|
|
4282
|
+
onChange: (e) => setEmail(e.target.value),
|
|
4283
|
+
style: { padding: 8, border: "1px solid rgba(15,23,42,0.15)", borderRadius: 6 }
|
|
1883
4284
|
}
|
|
1884
4285
|
),
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
top: 36,
|
|
1889
|
-
minWidth: 220,
|
|
1890
|
-
background: "#fff",
|
|
1891
|
-
border: "1px solid rgba(15,23,42,0.12)",
|
|
1892
|
-
borderRadius: 8,
|
|
1893
|
-
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
1894
|
-
padding: 8,
|
|
1895
|
-
zIndex: 100
|
|
1896
|
-
}, children: memberships.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6, padding: "4px 6px" }, children: "No memberships" }) : memberships.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1897
|
-
"button",
|
|
1898
|
-
{
|
|
1899
|
-
role: "menuitem",
|
|
1900
|
-
type: "button",
|
|
1901
|
-
onClick: () => switchTo(m.tenantId),
|
|
1902
|
-
style: {
|
|
1903
|
-
display: "block",
|
|
1904
|
-
width: "100%",
|
|
1905
|
-
textAlign: "left",
|
|
1906
|
-
padding: "8px 10px",
|
|
1907
|
-
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
1908
|
-
border: "none",
|
|
1909
|
-
borderRadius: 4,
|
|
1910
|
-
cursor: "pointer",
|
|
1911
|
-
fontSize: 13,
|
|
1912
|
-
color: "#0f172a"
|
|
1913
|
-
},
|
|
1914
|
-
children: [
|
|
1915
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
1916
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
1917
|
-
]
|
|
1918
|
-
},
|
|
1919
|
-
m.tenantId
|
|
1920
|
-
)) }) : null
|
|
4286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { "data-testid": "button-magic-link-submit", type: "submit", disabled: busy || !email, children: busy ? "Sending\u2026" : props.buttonLabel ?? "Email me a sign-in link" }),
|
|
4287
|
+
sent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-magic-link-sent", style: { fontSize: 12 }, children: "If the email is on file, a link is on the way." }) : null,
|
|
4288
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-magic-link-error", style: { fontSize: 12, color: "#b91c1c" }, children: error }) : null
|
|
1921
4289
|
]
|
|
1922
4290
|
}
|
|
1923
4291
|
);
|
|
1924
4292
|
}
|
|
4293
|
+
function PasskeySignInButton({ email, className, children, ...rest }) {
|
|
4294
|
+
const { signIn: signIn2, busy, error } = usePasskey(rest);
|
|
4295
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
|
|
4296
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4297
|
+
"button",
|
|
4298
|
+
{
|
|
4299
|
+
"data-testid": "button-passkey-signin",
|
|
4300
|
+
disabled: busy,
|
|
4301
|
+
onClick: () => void signIn2({ email }).catch(() => {
|
|
4302
|
+
}),
|
|
4303
|
+
style: { padding: "8px 12px", borderRadius: 6, border: "1px solid rgba(15,23,42,0.15)", background: "transparent", cursor: "pointer" },
|
|
4304
|
+
children: busy ? "Verifying\u2026" : children ?? "Sign in with a passkey"
|
|
4305
|
+
}
|
|
4306
|
+
),
|
|
4307
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-passkey-error", style: { fontSize: 12, color: "#b91c1c", marginTop: 4 }, children: error }) : null
|
|
4308
|
+
] });
|
|
4309
|
+
}
|
|
4310
|
+
function LinkedAccounts({ className, onChange, ...rest }) {
|
|
4311
|
+
const { identities, loading, error, unlink } = useLinkedIdentities(rest);
|
|
4312
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "section-linked-accounts", className, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "#b91c1c" }, children: error }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: identities.map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { "data-testid": `row-identity-${i.provider}`, style: { display: "flex", justifyContent: "space-between", padding: "6px 0" }, children: [
|
|
4313
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
4314
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { textTransform: "capitalize" }, children: i.provider }),
|
|
4315
|
+
" ",
|
|
4316
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.7 }, children: i.label || i.providerUserId || "" })
|
|
4317
|
+
] }),
|
|
4318
|
+
i.canUnlink ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4319
|
+
"button",
|
|
4320
|
+
{
|
|
4321
|
+
"data-testid": `button-unlink-${i.provider}`,
|
|
4322
|
+
onClick: async () => {
|
|
4323
|
+
const pw = window.prompt("Confirm your password to unlink this identity") || void 0;
|
|
4324
|
+
try {
|
|
4325
|
+
await unlink(i.provider, pw);
|
|
4326
|
+
onChange?.();
|
|
4327
|
+
} catch {
|
|
4328
|
+
}
|
|
4329
|
+
},
|
|
4330
|
+
children: "Unlink"
|
|
4331
|
+
}
|
|
4332
|
+
) : null
|
|
4333
|
+
] }, i.id)) }) });
|
|
4334
|
+
}
|
|
1925
4335
|
var __version__ = "phase-bc-1.0.0";
|
|
1926
4336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1927
4337
|
0 && (module.exports = {
|
|
1928
4338
|
AuthCallback,
|
|
4339
|
+
CreateOrganization,
|
|
1929
4340
|
IQAuthLoaded,
|
|
1930
4341
|
IQAuthLoading,
|
|
1931
4342
|
IQAuthProvider,
|
|
4343
|
+
IQAuthReturnToBouncer,
|
|
4344
|
+
ImpersonationBanner,
|
|
4345
|
+
LinkedAccounts,
|
|
4346
|
+
MagicLinkSignInForm,
|
|
4347
|
+
MultisessionAppSupport,
|
|
4348
|
+
OrganizationList,
|
|
4349
|
+
OrganizationProfile,
|
|
1932
4350
|
OrganizationSwitcher,
|
|
4351
|
+
PasskeySignInButton,
|
|
4352
|
+
Protect,
|
|
1933
4353
|
RedirectToSignIn,
|
|
4354
|
+
RedirectToSignedIn,
|
|
1934
4355
|
SignIn,
|
|
1935
4356
|
SignUp,
|
|
1936
4357
|
SignedIn,
|
|
1937
4358
|
SignedOut,
|
|
1938
4359
|
UserButton,
|
|
1939
4360
|
UserProfile,
|
|
4361
|
+
Waitlist,
|
|
1940
4362
|
__version__,
|
|
4363
|
+
isReturnToAllowed,
|
|
1941
4364
|
isSilentSsoEligible,
|
|
4365
|
+
preflightReturnTo,
|
|
4366
|
+
revokeSession,
|
|
1942
4367
|
sanitizeBrandCss,
|
|
4368
|
+
sanitizeReturnTo,
|
|
4369
|
+
slugify,
|
|
4370
|
+
useAccountList,
|
|
4371
|
+
useAccountSwitcher,
|
|
1943
4372
|
useAuth,
|
|
1944
4373
|
useAuthFetch,
|
|
1945
4374
|
useIQAuthSignInContext,
|
|
4375
|
+
useImpersonation,
|
|
4376
|
+
useLinkedIdentities,
|
|
4377
|
+
useLocale,
|
|
4378
|
+
useMagicLink,
|
|
1946
4379
|
useOrganization,
|
|
4380
|
+
usePasskey,
|
|
1947
4381
|
useResolvedSdkBranding,
|
|
4382
|
+
useReturnTo,
|
|
4383
|
+
useReverification,
|
|
1948
4384
|
useSession,
|
|
4385
|
+
useSessionList,
|
|
4386
|
+
useT,
|
|
1949
4387
|
useUser
|
|
1950
4388
|
});
|