@iqauth/sdk 2.3.0 → 2.5.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 +2985 -567
- package/dist/react.mjs +1517 -94
- 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
|
-
|
|
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);
|
|
738
1783
|
}
|
|
739
|
-
|
|
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 });
|
|
745
|
-
}
|
|
746
|
-
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
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;
|
|
@@ -945,7 +2210,114 @@ function RedirectToSignIn(props = {}) {
|
|
|
945
2210
|
)
|
|
946
2211
|
);
|
|
947
2212
|
}
|
|
948
|
-
return null;
|
|
2213
|
+
return null;
|
|
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
|
+
}
|
|
949
2321
|
}
|
|
950
2322
|
function AuthCallback({ onComplete, fallback } = {}) {
|
|
951
2323
|
const { manager } = useCtx();
|
|
@@ -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,8 @@ 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((r) => r.json()).then((payload) => {
|
|
1006
2378
|
if (cancelled) return;
|
|
1007
2379
|
if (payload?.success === false) throw new Error(payload?.error?.message || "Failed to load sign-in context");
|
|
1008
2380
|
setCtx(payload.data);
|
|
@@ -1164,34 +2536,34 @@ function flattenBrandingPayload(data) {
|
|
|
1164
2536
|
function useResolvedSdkBranding(iqAuthBaseUrl, appId) {
|
|
1165
2537
|
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
1166
2538
|
const resolvedAppId = appId ?? ctx?.manager?.appKey ?? null;
|
|
1167
|
-
const
|
|
1168
|
-
const cached = sdkBrandingCache.get(
|
|
2539
|
+
const url2 = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/public/branding${resolvedAppId ? `?appId=${encodeURIComponent(resolvedAppId)}` : ""}`;
|
|
2540
|
+
const cached = sdkBrandingCache.get(url2);
|
|
1169
2541
|
const fresh = cached && Date.now() - cached.ts < SDK_BRANDING_TTL_MS ? cached.data : null;
|
|
1170
2542
|
const [b, setB] = (0, import_react.useState)(fresh);
|
|
1171
2543
|
(0, import_react.useEffect)(() => {
|
|
1172
2544
|
let cancelled = false;
|
|
1173
|
-
const entry = sdkBrandingCache.get(
|
|
1174
|
-
const
|
|
1175
|
-
if (entry?.rev)
|
|
2545
|
+
const entry = sdkBrandingCache.get(url2);
|
|
2546
|
+
const headers2 = {};
|
|
2547
|
+
if (entry?.rev) headers2["If-None-Match"] = `W/"brand-${entry.rev}"`;
|
|
1176
2548
|
if (entry) setB(entry.data);
|
|
1177
|
-
fetch(
|
|
2549
|
+
fetch(url2, { credentials: "include", headers: headers2 }).then(async (r) => {
|
|
1178
2550
|
if (cancelled) return;
|
|
1179
2551
|
if (r.status === 304 && entry) {
|
|
1180
|
-
sdkBrandingCache.set(
|
|
2552
|
+
sdkBrandingCache.set(url2, { ...entry, ts: Date.now() });
|
|
1181
2553
|
return;
|
|
1182
2554
|
}
|
|
1183
2555
|
if (!r.ok) return;
|
|
1184
2556
|
const p = await r.json().catch(() => null);
|
|
1185
2557
|
if (!p?.data) return;
|
|
1186
2558
|
const flat = flattenBrandingPayload(p.data);
|
|
1187
|
-
sdkBrandingCache.set(
|
|
2559
|
+
sdkBrandingCache.set(url2, { ts: Date.now(), rev: flat.brandingRev || "", data: flat });
|
|
1188
2560
|
setB(flat);
|
|
1189
2561
|
}).catch(() => {
|
|
1190
2562
|
});
|
|
1191
2563
|
return () => {
|
|
1192
2564
|
cancelled = true;
|
|
1193
2565
|
};
|
|
1194
|
-
}, [
|
|
2566
|
+
}, [url2]);
|
|
1195
2567
|
return b;
|
|
1196
2568
|
}
|
|
1197
2569
|
function ensureSdkShellStyles() {
|
|
@@ -1232,10 +2604,12 @@ function Shell({
|
|
|
1232
2604
|
className,
|
|
1233
2605
|
children,
|
|
1234
2606
|
title,
|
|
1235
|
-
subtitle
|
|
2607
|
+
subtitle,
|
|
2608
|
+
appearance
|
|
1236
2609
|
}) {
|
|
1237
2610
|
ensureSdkShellStyles();
|
|
1238
|
-
|
|
2611
|
+
const t2 = useT();
|
|
2612
|
+
useDocumentBranding(branding, title || t2("signIn.title"));
|
|
1239
2613
|
const brandVars = brandStyle(branding);
|
|
1240
2614
|
const brandName = branding?.brandName || "IQAuth";
|
|
1241
2615
|
const heroImage = branding?.heroImageUrl || null;
|
|
@@ -1246,14 +2620,16 @@ function Shell({
|
|
|
1246
2620
|
const heroStyle = heroImage ? { ["--iqauth-sdk-hero-image"]: `url("${heroImage.replace(/"/g, '\\"')}")` } : {};
|
|
1247
2621
|
const shellStyle = {
|
|
1248
2622
|
...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, '\\"')}")` } : {}
|
|
2623
|
+
...layout === "full_bleed" && bgImage ? { backgroundImage: `linear-gradient(rgba(15,23,42,0.35), rgba(15,23,42,0.45)), url("${bgImage.replace(/"/g, '\\"')}")` } : {},
|
|
2624
|
+
...appearance?.elements?.rootBox?.style || {}
|
|
1250
2625
|
};
|
|
2626
|
+
const ap = appearance?.elements;
|
|
1251
2627
|
const supportLink = branding?.supportUrl ? branding.supportUrl : branding?.supportEmail ? `mailto:${branding.supportEmail}` : null;
|
|
1252
2628
|
const hasFooterLinks = !!(branding?.termsUrl || branding?.privacyUrl || supportLink);
|
|
1253
2629
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1254
2630
|
"div",
|
|
1255
2631
|
{
|
|
1256
|
-
className: `iqauth-sdk-shell${className ? ` ${className}` : ""}`,
|
|
2632
|
+
className: `iqauth-sdk-shell${className ? ` ${className}` : ""}${ap?.rootBox?.className ? ` ${ap.rootBox.className}` : ""}`,
|
|
1257
2633
|
"data-layout": layout,
|
|
1258
2634
|
"data-social-style": socialStyle || void 0,
|
|
1259
2635
|
style: shellStyle,
|
|
@@ -1274,13 +2650,34 @@ function Shell({
|
|
|
1274
2650
|
] }),
|
|
1275
2651
|
/* @__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
2652
|
/* @__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
|
-
|
|
2653
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2654
|
+
"section",
|
|
2655
|
+
{
|
|
2656
|
+
className: `iqauth-sdk-card${ap?.card?.className ? ` ${ap.card.className}` : ""}`,
|
|
2657
|
+
style: ap?.card?.style,
|
|
2658
|
+
children: [
|
|
2659
|
+
title || subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2660
|
+
"div",
|
|
2661
|
+
{
|
|
2662
|
+
className: `iqauth-sdk-card-header${ap?.cardHeader?.className ? ` ${ap.cardHeader.className}` : ""}`,
|
|
2663
|
+
style: ap?.cardHeader?.style,
|
|
2664
|
+
children: [
|
|
2665
|
+
title ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: ap?.headerTitle?.className, style: ap?.headerTitle?.style, children: title }) : null,
|
|
2666
|
+
subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: ap?.headerSubtitle?.className, style: ap?.headerSubtitle?.style, children: subtitle }) : null
|
|
2667
|
+
]
|
|
2668
|
+
}
|
|
2669
|
+
) : null,
|
|
2670
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2671
|
+
"div",
|
|
2672
|
+
{
|
|
2673
|
+
className: `iqauth-sdk-card-body${ap?.cardBody?.className ? ` ${ap.cardBody.className}` : ""}`,
|
|
2674
|
+
style: ap?.cardBody?.style,
|
|
2675
|
+
children
|
|
2676
|
+
}
|
|
2677
|
+
)
|
|
2678
|
+
]
|
|
2679
|
+
}
|
|
2680
|
+
),
|
|
1284
2681
|
hasFooterLinks || branding?.footerText ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("footer", { className: "iqauth-sdk-footer", children: [
|
|
1285
2682
|
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
2683
|
hasFooterLinks ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "iqauth-sdk-footer-links", children: [
|
|
@@ -1371,8 +2768,30 @@ function isSilentSsoEligible(ctx, effectivePrompt) {
|
|
|
1371
2768
|
if (!ctx.returnAllowed) return false;
|
|
1372
2769
|
return true;
|
|
1373
2770
|
}
|
|
1374
|
-
function SignIn(
|
|
2771
|
+
function SignIn(props) {
|
|
2772
|
+
const providerCtx = (0, import_react.useContext)(IQAuthContext);
|
|
2773
|
+
const iqAuthBaseUrl = props.iqAuthBaseUrl ?? providerCtx?.manager.issuerUrl ?? "";
|
|
2774
|
+
const appKey = props.appKey ?? providerCtx?.manager.appKey ?? "";
|
|
2775
|
+
const returnTo = props.returnTo ?? (typeof window !== "undefined" ? `${window.location.origin}/api/iqauth/callback` : "");
|
|
2776
|
+
const { onRedirect, className, prompt, appearance: instanceAppearance } = props;
|
|
2777
|
+
const appearance = instanceAppearance && providerCtx?.appearance ? { elements: { ...providerCtx.appearance.elements, ...instanceAppearance.elements } } : instanceAppearance ?? providerCtx?.appearance ?? null;
|
|
2778
|
+
if (!iqAuthBaseUrl || !appKey) {
|
|
2779
|
+
console.error(
|
|
2780
|
+
"[IQAuth] <SignIn /> could not determine iqAuthBaseUrl/appKey. Either pass them explicitly OR wrap the component in <IQAuthProvider publishableKey=\u2026/>."
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
const t2 = useT();
|
|
2784
|
+
const localeBundle = useLocale();
|
|
1375
2785
|
const { ctx, loading, error } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo);
|
|
2786
|
+
const preflightLoggedRef = (0, import_react.useRef)(false);
|
|
2787
|
+
(0, import_react.useEffect)(() => {
|
|
2788
|
+
if (!ctx || preflightLoggedRef.current) return;
|
|
2789
|
+
if (ctx.returnAllowed) return;
|
|
2790
|
+
preflightLoggedRef.current = true;
|
|
2791
|
+
console.error(
|
|
2792
|
+
`[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"}].`
|
|
2793
|
+
);
|
|
2794
|
+
}, [ctx, returnTo]);
|
|
1376
2795
|
const [email, setEmail] = (0, import_react.useState)("");
|
|
1377
2796
|
const [password, setPassword] = (0, import_react.useState)("");
|
|
1378
2797
|
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
@@ -1431,9 +2850,9 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1431
2850
|
body: JSON.stringify({ email, password, ...oidcPayload() })
|
|
1432
2851
|
});
|
|
1433
2852
|
const payload = await r.json().catch(() => ({}));
|
|
1434
|
-
if (!handlePayload(payload)) setFormError(
|
|
2853
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1435
2854
|
} catch (err) {
|
|
1436
|
-
setFormError(err.message ||
|
|
2855
|
+
setFormError(err.message || t(localeBundle, "errors.network"));
|
|
1437
2856
|
}
|
|
1438
2857
|
setSubmitting(false);
|
|
1439
2858
|
};
|
|
@@ -1455,7 +2874,7 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1455
2874
|
})
|
|
1456
2875
|
});
|
|
1457
2876
|
const payload = await r.json().catch(() => ({}));
|
|
1458
|
-
if (!handlePayload(payload)) setFormError(
|
|
2877
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1459
2878
|
setSubmitting(false);
|
|
1460
2879
|
};
|
|
1461
2880
|
const submitTenant = async (tenantId) => {
|
|
@@ -1469,7 +2888,7 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1469
2888
|
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
|
|
1470
2889
|
});
|
|
1471
2890
|
const payload = await r.json().catch(() => ({}));
|
|
1472
|
-
if (!handlePayload(payload)) setFormError(
|
|
2891
|
+
if (!handlePayload(payload)) setFormError(localizeError(localeBundle, { code: payload.error, message: payload.error_description }));
|
|
1473
2892
|
setSubmitting(false);
|
|
1474
2893
|
};
|
|
1475
2894
|
const startGoogleLogin = () => {
|
|
@@ -1478,8 +2897,8 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1478
2897
|
return;
|
|
1479
2898
|
}
|
|
1480
2899
|
const bridgeUrl = window.location.href;
|
|
1481
|
-
const
|
|
1482
|
-
window.location.href =
|
|
2900
|
+
const url2 = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/google?redirect_uri=${encodeURIComponent(bridgeUrl)}&client_id=${encodeURIComponent(ctx.app.defaultClientId)}`;
|
|
2901
|
+
window.location.href = url2;
|
|
1483
2902
|
};
|
|
1484
2903
|
(0, import_react.useEffect)(() => {
|
|
1485
2904
|
if (loading || error || !ctx) return;
|
|
@@ -1567,36 +2986,36 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1567
2986
|
setOauthExchanging(false);
|
|
1568
2987
|
})();
|
|
1569
2988
|
}, [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: "
|
|
2989
|
+
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") }) });
|
|
2990
|
+
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") }) });
|
|
2991
|
+
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
2992
|
const silentEligible = isSilentSsoEligible(ctx, effectivePrompt);
|
|
1574
2993
|
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: "
|
|
2994
|
+
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: [
|
|
2995
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-silent-resume", style: { fontSize: 14, opacity: 0.8 }, children: t2("signIn.resumingSession") }),
|
|
2996
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "#", onClick: switchAccount, "data-testid": "link-switch-account", style: { fontSize: 13 }, children: t2("signIn.useDifferentAccount") })
|
|
1578
2997
|
] });
|
|
1579
2998
|
}
|
|
1580
|
-
const cardTitle = ctx.branding?.loginHeadline ||
|
|
2999
|
+
const cardTitle = ctx.branding?.loginHeadline || t2("signIn.titleWithApp", { appName: ctx.app.name });
|
|
1581
3000
|
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: [
|
|
3001
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { appearance, branding: ctx.branding, className, title: cardTitle, subtitle: cardSubtitle, children: [
|
|
1583
3002
|
formError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: formError }) : null,
|
|
1584
|
-
tenantSel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-label": "
|
|
3003
|
+
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
3004
|
"button",
|
|
1586
3005
|
{
|
|
1587
3006
|
type: "button",
|
|
1588
|
-
"data-iqauth-tenant":
|
|
1589
|
-
onClick: () => submitTenant(
|
|
3007
|
+
"data-iqauth-tenant": tn.tenantId,
|
|
3008
|
+
onClick: () => submitTenant(tn.tenantId),
|
|
1590
3009
|
style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
|
|
1591
3010
|
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:
|
|
3011
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: tn.tenantName || tn.tenantSlug || tn.tenantId }),
|
|
3012
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: tn.roles.join(", ") })
|
|
1594
3013
|
]
|
|
1595
3014
|
},
|
|
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 ? "
|
|
3015
|
+
tn.tenantId
|
|
3016
|
+
)) }) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": t2("mfa.title"), children: [
|
|
3017
|
+
!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,
|
|
3018
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? t2("mfa.backupCodeLabel") : t2("mfa.totpLabel"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1600
3019
|
"input",
|
|
1601
3020
|
{
|
|
1602
3021
|
style: { ...inputStyle(), fontFamily: "monospace", textAlign: mfa.backup ? "left" : "center", letterSpacing: mfa.backup ? "0.04em" : "0.3em" },
|
|
@@ -1606,34 +3025,33 @@ function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className, prompt
|
|
|
1606
3025
|
inputMode: mfa.backup ? "text" : "numeric"
|
|
1607
3026
|
}
|
|
1608
3027
|
) }),
|
|
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 ? "
|
|
3028
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !mfa.code, children: submitting ? t2("mfa.submitting") : t2("mfa.submit") }),
|
|
3029
|
+
/* @__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
3030
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
1612
3031
|
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 || "
|
|
3032
|
+
/* @__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
3033
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": "true", children: [
|
|
1615
3034
|
/* @__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
3035
|
/* @__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
3036
|
/* @__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
3037
|
/* @__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
3038
|
] }),
|
|
1620
|
-
ctx.branding?.googleButtonLabel || "
|
|
3039
|
+
ctx.branding?.googleButtonLabel || t2("signIn.continueWithGoogle")
|
|
1621
3040
|
] }),
|
|
1622
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", "aria-label": "or", className: "iqauth-sdk-divider", children: "
|
|
3041
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "separator", "aria-label": t2("common.or"), className: "iqauth-sdk-divider", children: t2("signIn.dividerOr").toUpperCase() })
|
|
1623
3042
|
] }) : 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 ? "
|
|
3043
|
+
/* @__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: [
|
|
3044
|
+
/* @__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) }) }),
|
|
3045
|
+
/* @__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) }) }),
|
|
3046
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password, children: submitting ? t2("signIn.submitting") : t2("signIn.submit") })
|
|
1628
3047
|
] })
|
|
1629
3048
|
] }),
|
|
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
|
|
3049
|
+
(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
3050
|
] });
|
|
1635
3051
|
}
|
|
1636
3052
|
function SignUp({ iqAuthBaseUrl, appKey, returnTo, onSuccess, className }) {
|
|
3053
|
+
const t2 = useT();
|
|
3054
|
+
const localeBundle = useLocale();
|
|
1637
3055
|
const { ctx, loading } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo || "");
|
|
1638
3056
|
const [name, setName] = (0, import_react.useState)("");
|
|
1639
3057
|
const [email, setEmail] = (0, import_react.useState)("");
|
|
@@ -1655,22 +3073,19 @@ function SignUp({ iqAuthBaseUrl, appKey, returnTo, onSuccess, className }) {
|
|
|
1655
3073
|
setDone(true);
|
|
1656
3074
|
onSuccess?.();
|
|
1657
3075
|
} catch (err) {
|
|
1658
|
-
setError(err.message);
|
|
3076
|
+
setError(localizeError(localeBundle, err.message));
|
|
1659
3077
|
}
|
|
1660
3078
|
setSubmitting(false);
|
|
1661
3079
|
};
|
|
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
|
-
] });
|
|
3080
|
+
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") }) });
|
|
3081
|
+
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: [
|
|
3082
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
3083
|
+
/* @__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 }) }),
|
|
3084
|
+
/* @__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 }) }),
|
|
3085
|
+
/* @__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) }) }),
|
|
3086
|
+
/* @__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 }) }),
|
|
3087
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password || !name, children: submitting ? t2("signUp.submitting") : t2("signUp.submit") })
|
|
3088
|
+
] }) });
|
|
1674
3089
|
}
|
|
1675
3090
|
function initialsOf(name, email) {
|
|
1676
3091
|
const src = name || email || "?";
|
|
@@ -1679,6 +3094,7 @@ function initialsOf(name, email) {
|
|
|
1679
3094
|
return src.substring(0, 2).toUpperCase();
|
|
1680
3095
|
}
|
|
1681
3096
|
function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
3097
|
+
const t2 = useT();
|
|
1682
3098
|
const [user, setUser] = (0, import_react.useState)(null);
|
|
1683
3099
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1684
3100
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
@@ -1749,7 +3165,7 @@ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
|
1749
3165
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500, color: "#0f172a" }, children: user.name }),
|
|
1750
3166
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: user.email })
|
|
1751
3167
|
] }),
|
|
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: "
|
|
3168
|
+
/* @__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
3169
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1754
3170
|
"button",
|
|
1755
3171
|
{
|
|
@@ -1757,7 +3173,7 @@ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
|
1757
3173
|
type: "button",
|
|
1758
3174
|
onClick: signOut2,
|
|
1759
3175
|
style: { display: "block", width: "100%", textAlign: "left", padding: "8px 10px", fontSize: 13, background: "transparent", border: "none", cursor: "pointer", color: "#b91c1c" },
|
|
1760
|
-
children: "
|
|
3176
|
+
children: t2("userButton.signOut")
|
|
1761
3177
|
}
|
|
1762
3178
|
)
|
|
1763
3179
|
] }) : null
|
|
@@ -1766,185 +3182,1187 @@ function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
|
1766
3182
|
);
|
|
1767
3183
|
}
|
|
1768
3184
|
function UserProfile({ iqAuthBaseUrl, className }) {
|
|
3185
|
+
const t2 = useT();
|
|
3186
|
+
const localeBundle = useLocale();
|
|
1769
3187
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
1770
3188
|
const [user, setUser] = (0, import_react.useState)(null);
|
|
1771
3189
|
const [oldPassword, setOldPassword] = (0, import_react.useState)("");
|
|
1772
3190
|
const [newPassword, setNewPassword] = (0, import_react.useState)("");
|
|
1773
3191
|
const [pwState, setPwState] = (0, import_react.useState)({ submitting: false, message: "", error: "" });
|
|
1774
3192
|
const [sessions, setSessions] = (0, import_react.useState)([]);
|
|
3193
|
+
const [revokeAllBusy, setRevokeAllBusy] = (0, import_react.useState)(false);
|
|
3194
|
+
const loadSessions = () => {
|
|
3195
|
+
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(() => {
|
|
3196
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions`, { credentials: "include" }).then((r) => r.json()).then((p) => setSessions(p?.data?.sessions || p?.data || [])).catch(() => {
|
|
3197
|
+
});
|
|
3198
|
+
});
|
|
3199
|
+
};
|
|
3200
|
+
(0, import_react.useEffect)(() => {
|
|
3201
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
3202
|
+
if (p?.data) setUser(p.data);
|
|
3203
|
+
}).catch(() => {
|
|
3204
|
+
});
|
|
3205
|
+
loadSessions();
|
|
3206
|
+
}, [iqAuthBaseUrl]);
|
|
3207
|
+
const changePassword = async (e) => {
|
|
3208
|
+
e.preventDefault();
|
|
3209
|
+
setPwState({ submitting: true, message: "", error: "" });
|
|
3210
|
+
try {
|
|
3211
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/password/change`, {
|
|
3212
|
+
method: "POST",
|
|
3213
|
+
headers: { "Content-Type": "application/json" },
|
|
3214
|
+
body: JSON.stringify({ oldPassword, newPassword })
|
|
3215
|
+
});
|
|
3216
|
+
setPwState({ submitting: false, message: t(localeBundle, "userProfile.passwordUpdated"), error: "" });
|
|
3217
|
+
setOldPassword("");
|
|
3218
|
+
setNewPassword("");
|
|
3219
|
+
} catch (err) {
|
|
3220
|
+
setPwState({ submitting: false, message: "", error: localizeError(localeBundle, err.message) });
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
const revoke = async (sessionId) => {
|
|
3224
|
+
const newRes = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
|
|
3225
|
+
if (!newRes.ok && newRes.status === 404) {
|
|
3226
|
+
await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
|
|
3227
|
+
}
|
|
3228
|
+
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
|
3229
|
+
};
|
|
3230
|
+
const revokeAllOthers = async () => {
|
|
3231
|
+
setRevokeAllBusy(true);
|
|
3232
|
+
try {
|
|
3233
|
+
await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/users/me/sessions/revoke-all-others`, { method: "POST", credentials: "include" });
|
|
3234
|
+
setSessions((prev) => prev.filter((s) => s.isCurrent));
|
|
3235
|
+
} finally {
|
|
3236
|
+
setRevokeAllBusy(false);
|
|
3237
|
+
}
|
|
3238
|
+
};
|
|
3239
|
+
if (!user) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t2("common.loading") }) });
|
|
3240
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding, className, title: t2("userProfile.title"), children: [
|
|
3241
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-profile", style: { marginBottom: 20 }, children: [
|
|
3242
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-profile", style: { fontSize: 14, fontWeight: 600 }, children: t2("userProfile.profileTab") }),
|
|
3243
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
|
|
3244
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
3245
|
+
t2("common.name"),
|
|
3246
|
+
":"
|
|
3247
|
+
] }),
|
|
3248
|
+
" ",
|
|
3249
|
+
user.name
|
|
3250
|
+
] }),
|
|
3251
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
|
|
3252
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
3253
|
+
t2("common.email"),
|
|
3254
|
+
":"
|
|
3255
|
+
] }),
|
|
3256
|
+
" ",
|
|
3257
|
+
user.email
|
|
3258
|
+
] })
|
|
3259
|
+
] }),
|
|
3260
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-pw", style: { marginBottom: 20 }, children: [
|
|
3261
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-pw", style: { fontSize: 14, fontWeight: 600 }, children: t2("userProfile.changePassword") }),
|
|
3262
|
+
pwState.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: pwState.error }) : null,
|
|
3263
|
+
pwState.message ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", style: { fontSize: 13, color: "#047857" }, children: pwState.message }) : null,
|
|
3264
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: changePassword, style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
|
|
3265
|
+
/* @__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 }) }),
|
|
3266
|
+
/* @__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 }) }),
|
|
3267
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: pwState.submitting || !oldPassword || !newPassword, children: pwState.submitting ? t2("common.saving") : t2("userProfile.changePassword") })
|
|
3268
|
+
] })
|
|
3269
|
+
] }),
|
|
3270
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-sessions", children: [
|
|
3271
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
|
|
3272
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-sessions", style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: t2("userProfile.sessionsTab") }),
|
|
3273
|
+
sessions.some((s) => !s.isCurrent) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3274
|
+
"button",
|
|
3275
|
+
{
|
|
3276
|
+
type: "button",
|
|
3277
|
+
disabled: revokeAllBusy,
|
|
3278
|
+
onClick: revokeAllOthers,
|
|
3279
|
+
style: { fontSize: 12, color: "#b91c1c", background: "transparent", border: "1px solid #fecaca", borderRadius: 4, padding: "4px 10px", cursor: "pointer" },
|
|
3280
|
+
children: revokeAllBusy ? t2("common.submitting") : t2("userProfile.revokeAllOthers")
|
|
3281
|
+
}
|
|
3282
|
+
)
|
|
3283
|
+
] }),
|
|
3284
|
+
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: [
|
|
3285
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: [
|
|
3286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontWeight: 500 }, children: [
|
|
3287
|
+
s.device || s.userAgent || s.deviceName || "\u2014",
|
|
3288
|
+
s.isCurrent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: 8, fontSize: 11, color: "#047857" }, children: [
|
|
3289
|
+
"(",
|
|
3290
|
+
t2("userProfile.thisDevice"),
|
|
3291
|
+
")"
|
|
3292
|
+
] })
|
|
3293
|
+
] }),
|
|
3294
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: 11, opacity: 0.65 }, children: [
|
|
3295
|
+
s.ip || "\u2014",
|
|
3296
|
+
s.lastActiveAt ? ` \xB7 ${new Date(s.lastActiveAt).toLocaleString()}` : ""
|
|
3297
|
+
] })
|
|
3298
|
+
] }),
|
|
3299
|
+
!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") })
|
|
3300
|
+
] }, s.id)) })
|
|
3301
|
+
] })
|
|
3302
|
+
] });
|
|
3303
|
+
}
|
|
3304
|
+
function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, appearance: _appearance, className }) {
|
|
3305
|
+
const t2 = useT();
|
|
3306
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3307
|
+
const accent = branding?.accentColor || "#6366f1";
|
|
3308
|
+
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
3309
|
+
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
3310
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
3311
|
+
(0, import_react.useEffect)(() => {
|
|
3312
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
3313
|
+
if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
|
|
3314
|
+
}).catch(() => {
|
|
3315
|
+
});
|
|
3316
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json()).then((p) => setMemberships(p?.data?.memberships || p?.data || [])).catch(() => {
|
|
3317
|
+
});
|
|
3318
|
+
}, [iqAuthBaseUrl]);
|
|
3319
|
+
const switchTo = async (tenantId) => {
|
|
3320
|
+
try {
|
|
3321
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
|
|
3322
|
+
method: "POST",
|
|
3323
|
+
headers: { "Content-Type": "application/json" },
|
|
3324
|
+
body: JSON.stringify({ tenantId })
|
|
3325
|
+
});
|
|
3326
|
+
setActiveTenantId(tenantId);
|
|
3327
|
+
setOpen(false);
|
|
3328
|
+
onSwitched?.(tenantId);
|
|
3329
|
+
} catch {
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
const active = memberships.find((m) => m.tenantId === activeTenantId);
|
|
3333
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3334
|
+
"div",
|
|
3335
|
+
{
|
|
3336
|
+
className,
|
|
3337
|
+
style: { position: "relative", display: "inline-block", ...brandStyle(branding) },
|
|
3338
|
+
"data-iqauth-sdk-orgswitcher": "",
|
|
3339
|
+
"data-branding-rev": branding?.brandingRev || "",
|
|
3340
|
+
children: [
|
|
3341
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3342
|
+
"button",
|
|
3343
|
+
{
|
|
3344
|
+
type: "button",
|
|
3345
|
+
"aria-haspopup": "menu",
|
|
3346
|
+
"aria-expanded": open,
|
|
3347
|
+
onClick: () => setOpen((o) => !o),
|
|
3348
|
+
style: { background: "transparent", border: `1px solid ${accent}55`, color: branding?.primaryColor || "#0f172a", padding: "6px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13 },
|
|
3349
|
+
children: active?.tenantName || active?.tenantSlug || t2("orgSwitcher.label")
|
|
3350
|
+
}
|
|
3351
|
+
),
|
|
3352
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", style: {
|
|
3353
|
+
position: "absolute",
|
|
3354
|
+
left: 0,
|
|
3355
|
+
top: 36,
|
|
3356
|
+
minWidth: 220,
|
|
3357
|
+
background: "#fff",
|
|
3358
|
+
border: "1px solid rgba(15,23,42,0.12)",
|
|
3359
|
+
borderRadius: 8,
|
|
3360
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
3361
|
+
padding: 8,
|
|
3362
|
+
zIndex: 100
|
|
3363
|
+
}, 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)(
|
|
3364
|
+
"button",
|
|
3365
|
+
{
|
|
3366
|
+
role: "menuitem",
|
|
3367
|
+
type: "button",
|
|
3368
|
+
onClick: () => switchTo(m.tenantId),
|
|
3369
|
+
style: {
|
|
3370
|
+
display: "block",
|
|
3371
|
+
width: "100%",
|
|
3372
|
+
textAlign: "left",
|
|
3373
|
+
padding: "8px 10px",
|
|
3374
|
+
background: m.tenantId === activeTenantId ? `${accent}14` : "transparent",
|
|
3375
|
+
border: "none",
|
|
3376
|
+
borderRadius: 4,
|
|
3377
|
+
cursor: "pointer",
|
|
3378
|
+
fontSize: 13,
|
|
3379
|
+
color: "#0f172a"
|
|
3380
|
+
},
|
|
3381
|
+
children: [
|
|
3382
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
3383
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
3384
|
+
]
|
|
3385
|
+
},
|
|
3386
|
+
m.tenantId
|
|
3387
|
+
)) }) : null
|
|
3388
|
+
]
|
|
3389
|
+
}
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
function useImpersonation() {
|
|
3393
|
+
const { snapshot } = useCtx();
|
|
3394
|
+
return (0, import_react.useMemo)(() => {
|
|
3395
|
+
const claims = snapshot.claims;
|
|
3396
|
+
const isImpersonating = claims?.purpose === "impersonation" && !!claims?.act?.sub;
|
|
3397
|
+
return {
|
|
3398
|
+
isImpersonating,
|
|
3399
|
+
actor: isImpersonating ? claims.act : null,
|
|
3400
|
+
target: isImpersonating ? snapshot.user : null
|
|
3401
|
+
};
|
|
3402
|
+
}, [snapshot]);
|
|
3403
|
+
}
|
|
3404
|
+
function ImpersonationBanner({ render, onExit, className, style } = {}) {
|
|
3405
|
+
const t2 = useT();
|
|
3406
|
+
const info = useImpersonation();
|
|
3407
|
+
const { manager } = useCtx();
|
|
3408
|
+
const exit = (0, import_react.useCallback)(async () => {
|
|
3409
|
+
if (onExit) return void onExit();
|
|
3410
|
+
const { exitImpersonation: exitImpersonation2 } = await Promise.resolve().then(() => (init_reverify(), reverify_exports));
|
|
3411
|
+
const restored = exitImpersonation2(manager);
|
|
3412
|
+
if (restored) return;
|
|
3413
|
+
const { signOut: signOut2 } = await Promise.resolve().then(() => (init_signIn(), signIn_exports));
|
|
3414
|
+
await signOut2(manager);
|
|
3415
|
+
}, [manager, onExit]);
|
|
3416
|
+
if (!info.isImpersonating) return null;
|
|
3417
|
+
if (render) return (0, import_react.createElement)(import_react.Fragment, null, render({ ...info, exit }));
|
|
3418
|
+
const targetLabel = info.target?.email || info.target?.name || info.target?.sub || "user";
|
|
3419
|
+
const _actorLabel = info.actor?.email || info.actor?.name || info.actor?.sub || "admin";
|
|
3420
|
+
void _actorLabel;
|
|
3421
|
+
return (0, import_react.createElement)(
|
|
3422
|
+
"div",
|
|
3423
|
+
{
|
|
3424
|
+
role: "alert",
|
|
3425
|
+
className,
|
|
3426
|
+
style: {
|
|
3427
|
+
position: "sticky",
|
|
3428
|
+
top: 0,
|
|
3429
|
+
left: 0,
|
|
3430
|
+
right: 0,
|
|
3431
|
+
zIndex: 9999,
|
|
3432
|
+
background: "#b91c1c",
|
|
3433
|
+
color: "#fff",
|
|
3434
|
+
padding: "8px 16px",
|
|
3435
|
+
display: "flex",
|
|
3436
|
+
alignItems: "center",
|
|
3437
|
+
justifyContent: "space-between",
|
|
3438
|
+
fontSize: 13,
|
|
3439
|
+
fontFamily: "system-ui, sans-serif",
|
|
3440
|
+
...style
|
|
3441
|
+
}
|
|
3442
|
+
},
|
|
3443
|
+
(0, import_react.createElement)(
|
|
3444
|
+
"span",
|
|
3445
|
+
null,
|
|
3446
|
+
t2("impersonation.banner", { targetEmail: targetLabel })
|
|
3447
|
+
),
|
|
3448
|
+
(0, import_react.createElement)(
|
|
3449
|
+
"button",
|
|
3450
|
+
{
|
|
3451
|
+
type: "button",
|
|
3452
|
+
onClick: exit,
|
|
3453
|
+
style: {
|
|
3454
|
+
background: "rgba(255,255,255,0.18)",
|
|
3455
|
+
color: "#fff",
|
|
3456
|
+
border: "1px solid rgba(255,255,255,0.4)",
|
|
3457
|
+
borderRadius: 4,
|
|
3458
|
+
padding: "4px 10px",
|
|
3459
|
+
cursor: "pointer",
|
|
3460
|
+
fontSize: 12
|
|
3461
|
+
}
|
|
3462
|
+
},
|
|
3463
|
+
t2("impersonation.exit")
|
|
3464
|
+
)
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
function useReverification(fn, opts = {}) {
|
|
3468
|
+
const { manager } = useCtx();
|
|
3469
|
+
const level = opts.level ?? "password";
|
|
3470
|
+
return (async (...args) => {
|
|
3471
|
+
let token = null;
|
|
3472
|
+
let res = await fn(token)(...args);
|
|
3473
|
+
const code = await peekErrorCode(res);
|
|
3474
|
+
const isReverifyError = res.status === 401 && code && (code === "REVERIFICATION_REQUIRED" || code === "REVERIFICATION_EXPIRED" || code === "REVERIFICATION_INVALID" || code === "REVERIFICATION_USED" || code === "REVERIFICATION_LEVEL_INSUFFICIENT");
|
|
3475
|
+
if (!isReverifyError) return res;
|
|
3476
|
+
const prompt = opts.prompt ?? defaultReverifyPrompt;
|
|
3477
|
+
const credentials = await prompt(level);
|
|
3478
|
+
if (!credentials) {
|
|
3479
|
+
throw new Error("Reverification cancelled");
|
|
3480
|
+
}
|
|
3481
|
+
const { reverify: reverify2 } = await Promise.resolve().then(() => (init_reverify(), reverify_exports));
|
|
3482
|
+
const minted = await reverify2(manager, { level, ...credentials });
|
|
3483
|
+
token = minted.token;
|
|
3484
|
+
res = await fn(token)(...args);
|
|
3485
|
+
return res;
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
async function peekErrorCode(res) {
|
|
3489
|
+
try {
|
|
3490
|
+
const cloned = res.clone();
|
|
3491
|
+
const body = await cloned.json();
|
|
3492
|
+
return body?.error?.code ?? null;
|
|
3493
|
+
} catch {
|
|
3494
|
+
return null;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
async function defaultReverifyPrompt(level) {
|
|
3498
|
+
if (typeof window === "undefined") return null;
|
|
3499
|
+
if (level === "password") {
|
|
3500
|
+
const password = window.prompt("Confirm your password to continue");
|
|
3501
|
+
if (!password) return null;
|
|
3502
|
+
return { password };
|
|
3503
|
+
}
|
|
3504
|
+
const totp = window.prompt("Enter your MFA code to continue");
|
|
3505
|
+
if (!totp) return null;
|
|
3506
|
+
return { totp, method: "totp" };
|
|
3507
|
+
}
|
|
3508
|
+
function slugify(input) {
|
|
3509
|
+
return input.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
3510
|
+
}
|
|
3511
|
+
function CreateOrganization({ iqAuthBaseUrl, onCreated, redirectUrl, unstyled, appearance, className }) {
|
|
3512
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3513
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
3514
|
+
const [slug, setSlug] = (0, import_react.useState)("");
|
|
3515
|
+
const [slugTouched, setSlugTouched] = (0, import_react.useState)(false);
|
|
3516
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
3517
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
3518
|
+
const [created, setCreated] = (0, import_react.useState)(null);
|
|
3519
|
+
const [slugCheck, setSlugCheck] = (0, import_react.useState)({ status: "idle" });
|
|
3520
|
+
(0, import_react.useEffect)(() => {
|
|
3521
|
+
if (!slugTouched) setSlug(slugify(name));
|
|
3522
|
+
}, [name, slugTouched]);
|
|
3523
|
+
(0, import_react.useEffect)(() => {
|
|
3524
|
+
const s = slug.trim().toLowerCase();
|
|
3525
|
+
if (!s) {
|
|
3526
|
+
setSlugCheck({ status: "idle" });
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
if (!/^[a-z0-9-]{2,64}$/.test(s)) {
|
|
3530
|
+
setSlugCheck({ status: "invalid", checked: s });
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
setSlugCheck({ status: "checking", checked: s });
|
|
3534
|
+
const handle = setTimeout(async () => {
|
|
3535
|
+
try {
|
|
3536
|
+
const res = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/check-slug?slug=${encodeURIComponent(s)}`, {
|
|
3537
|
+
credentials: "include",
|
|
3538
|
+
headers: { Accept: "application/json" }
|
|
3539
|
+
});
|
|
3540
|
+
const body = await res.json().catch(() => ({}));
|
|
3541
|
+
const data = body.data;
|
|
3542
|
+
if (!data) {
|
|
3543
|
+
setSlugCheck({ status: "idle", checked: s });
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (data.reason === "INVALID_FORMAT") {
|
|
3547
|
+
setSlugCheck({ status: "invalid", checked: s });
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
setSlugCheck({ status: data.available ? "available" : "taken", checked: s });
|
|
3551
|
+
} catch {
|
|
3552
|
+
setSlugCheck({ status: "idle", checked: s });
|
|
3553
|
+
}
|
|
3554
|
+
}, 350);
|
|
3555
|
+
return () => clearTimeout(handle);
|
|
3556
|
+
}, [slug, iqAuthBaseUrl]);
|
|
3557
|
+
const submit = async (e) => {
|
|
3558
|
+
e.preventDefault();
|
|
3559
|
+
setSubmitting(true);
|
|
3560
|
+
setError(null);
|
|
3561
|
+
try {
|
|
3562
|
+
const res = await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants`, {
|
|
3563
|
+
method: "POST",
|
|
3564
|
+
headers: { "Content-Type": "application/json" },
|
|
3565
|
+
credentials: "include",
|
|
3566
|
+
body: JSON.stringify({ name: name.trim(), slug: slug.trim() })
|
|
3567
|
+
});
|
|
3568
|
+
const payload = res;
|
|
3569
|
+
const tenant = payload.data ?? payload;
|
|
3570
|
+
const next = { id: tenant.id, name: tenant.name, slug: tenant.slug };
|
|
3571
|
+
setCreated(next);
|
|
3572
|
+
onCreated?.(next);
|
|
3573
|
+
setName("");
|
|
3574
|
+
setSlug("");
|
|
3575
|
+
setSlugTouched(false);
|
|
3576
|
+
if (redirectUrl && typeof window !== "undefined") {
|
|
3577
|
+
const url2 = typeof redirectUrl === "function" ? redirectUrl(next) : redirectUrl;
|
|
3578
|
+
if (url2) window.location.assign(url2);
|
|
3579
|
+
}
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
setError(err instanceof Error ? err.message : "Failed to create organization");
|
|
3582
|
+
} finally {
|
|
3583
|
+
setSubmitting(false);
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
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: [
|
|
3587
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
3588
|
+
created ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { role: "status", style: { fontSize: 13, color: "#047857" }, children: [
|
|
3589
|
+
"Organization \u201C",
|
|
3590
|
+
created.name,
|
|
3591
|
+
"\u201D created."
|
|
3592
|
+
] }) : null,
|
|
3593
|
+
/* @__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" }) }),
|
|
3594
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Field, { label: "Organization slug", children: [
|
|
3595
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3596
|
+
"input",
|
|
3597
|
+
{
|
|
3598
|
+
"data-testid": "input-create-org-slug",
|
|
3599
|
+
style: { ...inputStyle(), fontFamily: "monospace" },
|
|
3600
|
+
value: slug,
|
|
3601
|
+
onChange: (e) => {
|
|
3602
|
+
setSlugTouched(true);
|
|
3603
|
+
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"));
|
|
3604
|
+
},
|
|
3605
|
+
required: true,
|
|
3606
|
+
pattern: "[a-z0-9-]+",
|
|
3607
|
+
minLength: 2,
|
|
3608
|
+
"aria-required": "true",
|
|
3609
|
+
"aria-describedby": "iqauth-create-org-slug-hint",
|
|
3610
|
+
"aria-invalid": slugCheck.status === "taken" || slugCheck.status === "invalid"
|
|
3611
|
+
}
|
|
3612
|
+
),
|
|
3613
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3614
|
+
"p",
|
|
3615
|
+
{
|
|
3616
|
+
id: "iqauth-create-org-slug-hint",
|
|
3617
|
+
"data-testid": "text-create-org-slug-status",
|
|
3618
|
+
role: "status",
|
|
3619
|
+
"aria-live": "polite",
|
|
3620
|
+
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 },
|
|
3621
|
+
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."
|
|
3622
|
+
}
|
|
3623
|
+
)
|
|
3624
|
+
] }),
|
|
3625
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3626
|
+
PrimaryButton,
|
|
3627
|
+
{
|
|
3628
|
+
"data-testid": "button-create-org-submit",
|
|
3629
|
+
type: "submit",
|
|
3630
|
+
disabled: submitting || !name.trim() || !slug.trim() || slugCheck.status === "taken" || slugCheck.status === "invalid" || slugCheck.status === "checking",
|
|
3631
|
+
children: submitting ? "Creating\u2026" : "Create organization"
|
|
3632
|
+
}
|
|
3633
|
+
)
|
|
3634
|
+
] });
|
|
3635
|
+
if (unstyled) {
|
|
3636
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, "data-iqauth-sdk-create-org-bare": "", children: [
|
|
3637
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-create-org-heading", style: { fontSize: 14, fontWeight: 600, margin: "0 0 12px" }, children: "Create organization" }),
|
|
3638
|
+
form
|
|
3639
|
+
] });
|
|
3640
|
+
}
|
|
3641
|
+
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 });
|
|
3642
|
+
}
|
|
3643
|
+
function OrganizationProfile({ iqAuthBaseUrl, tenantId: tenantIdProp, tabs, onDeleted, appearance, className }) {
|
|
3644
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
3645
|
+
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
3646
|
+
const visibleTabs = tabs && tabs.length > 0 ? tabs : ["general", "members", "invitations", "danger"];
|
|
3647
|
+
const [activeTab, setActiveTab] = (0, import_react.useState)(visibleTabs[0]);
|
|
3648
|
+
const [tenantId, setTenantId] = (0, import_react.useState)(tenantIdProp || null);
|
|
3649
|
+
const [tenant, setTenant] = (0, import_react.useState)(null);
|
|
3650
|
+
const [members, setMembers] = (0, import_react.useState)([]);
|
|
3651
|
+
const [pendingInvites, setPendingInvites] = (0, import_react.useState)([]);
|
|
3652
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
3653
|
+
const [invitesLoading, setInvitesLoading] = (0, import_react.useState)(false);
|
|
3654
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
3655
|
+
const [renameValue, setRenameValue] = (0, import_react.useState)("");
|
|
3656
|
+
const [slugValue, setSlugValue] = (0, import_react.useState)("");
|
|
3657
|
+
const [renameSubmitting, setRenameSubmitting] = (0, import_react.useState)(false);
|
|
3658
|
+
const [inviteEmail, setInviteEmail] = (0, import_react.useState)("");
|
|
3659
|
+
const [inviteRole, setInviteRole] = (0, import_react.useState)("tenant_member");
|
|
3660
|
+
const [inviteSubmitting, setInviteSubmitting] = (0, import_react.useState)(false);
|
|
3661
|
+
const [actionMessage, setActionMessage] = (0, import_react.useState)(null);
|
|
3662
|
+
const [confirmDeleteText, setConfirmDeleteText] = (0, import_react.useState)("");
|
|
3663
|
+
const [confirmDeletePassword, setConfirmDeletePassword] = (0, import_react.useState)("");
|
|
3664
|
+
const [deleteSubmitting, setDeleteSubmitting] = (0, import_react.useState)(false);
|
|
3665
|
+
const { user } = useUser();
|
|
3666
|
+
const callerRole = user?.role || null;
|
|
3667
|
+
const callerIsAdmin = callerRole === "tenant_admin" || callerRole === "platform_admin";
|
|
3668
|
+
const visibleTabsFiltered = visibleTabs.filter((t2) => t2 !== "danger" || callerIsAdmin);
|
|
3669
|
+
(0, import_react.useEffect)(() => {
|
|
3670
|
+
let cancelled = false;
|
|
3671
|
+
(async () => {
|
|
3672
|
+
try {
|
|
3673
|
+
let tid = tenantIdProp || null;
|
|
3674
|
+
if (!tid) {
|
|
3675
|
+
const me = await fetch(`${baseUrl}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json());
|
|
3676
|
+
tid = me?.data?.tenantId || null;
|
|
3677
|
+
}
|
|
3678
|
+
if (!tid) {
|
|
3679
|
+
if (!cancelled) {
|
|
3680
|
+
setError("No active tenant");
|
|
3681
|
+
setLoading(false);
|
|
3682
|
+
}
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
const t2 = await fetch(`${baseUrl}/api/v1/tenants/${tid}`, { credentials: "include" }).then((r) => r.json());
|
|
3686
|
+
const m = await fetch(`${baseUrl}/api/v1/tenants/${tid}/users`, { credentials: "include" }).then((r) => r.json());
|
|
3687
|
+
if (cancelled) return;
|
|
3688
|
+
setTenantId(tid);
|
|
3689
|
+
setTenant(t2?.data ? { id: t2.data.id, name: t2.data.name, slug: t2.data.slug } : null);
|
|
3690
|
+
setRenameValue(t2?.data?.name || "");
|
|
3691
|
+
setSlugValue(t2?.data?.slug || "");
|
|
3692
|
+
const rows = m?.data || [];
|
|
3693
|
+
setMembers(rows.map((row) => ({
|
|
3694
|
+
userId: String(row.userId ?? row.user?.id ?? row.id ?? ""),
|
|
3695
|
+
email: String(row.email ?? row.user?.email ?? ""),
|
|
3696
|
+
name: String(row.name ?? row.user?.name ?? ""),
|
|
3697
|
+
role: String(row.role ?? row.roles?.[0] ?? "tenant_member"),
|
|
3698
|
+
joinedAt: row.joinedAt ?? row.createdAt
|
|
3699
|
+
})));
|
|
3700
|
+
} catch (err) {
|
|
3701
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load organization");
|
|
3702
|
+
} finally {
|
|
3703
|
+
if (!cancelled) setLoading(false);
|
|
3704
|
+
}
|
|
3705
|
+
})();
|
|
3706
|
+
return () => {
|
|
3707
|
+
cancelled = true;
|
|
3708
|
+
};
|
|
3709
|
+
}, [baseUrl, tenantIdProp]);
|
|
3710
|
+
const loadInvites = async (tid) => {
|
|
3711
|
+
setInvitesLoading(true);
|
|
3712
|
+
try {
|
|
3713
|
+
const r = await fetch(`${baseUrl}/api/v1/invites?tenantId=${encodeURIComponent(tid)}&status=pending`, { credentials: "include" }).then((res) => res.json());
|
|
3714
|
+
const rows = r?.data || [];
|
|
3715
|
+
setPendingInvites(rows.map((inv) => ({
|
|
3716
|
+
id: String(inv.id),
|
|
3717
|
+
email: String(inv.email),
|
|
3718
|
+
role: String(inv.role),
|
|
3719
|
+
status: String(inv.status),
|
|
3720
|
+
expiresAt: inv.expiresAt,
|
|
3721
|
+
createdAt: inv.createdAt
|
|
3722
|
+
})));
|
|
3723
|
+
} catch (err) {
|
|
3724
|
+
setError(err instanceof Error ? err.message : "Failed to load invitations");
|
|
3725
|
+
} finally {
|
|
3726
|
+
setInvitesLoading(false);
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
1775
3729
|
(0, import_react.useEffect)(() => {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
fetch(`${
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
3730
|
+
if (activeTab === "invitations" && tenantId) loadInvites(tenantId);
|
|
3731
|
+
}, [activeTab, tenantId]);
|
|
3732
|
+
const reloadMembers = async () => {
|
|
3733
|
+
if (!tenantId) return;
|
|
3734
|
+
const m = await fetch(`${baseUrl}/api/v1/tenants/${tenantId}/users`, { credentials: "include" }).then((r) => r.json());
|
|
3735
|
+
const rows = m?.data || [];
|
|
3736
|
+
setMembers(rows.map((row) => ({
|
|
3737
|
+
userId: String(row.userId ?? row.user?.id ?? row.id ?? ""),
|
|
3738
|
+
email: String(row.email ?? row.user?.email ?? ""),
|
|
3739
|
+
name: String(row.name ?? row.user?.name ?? ""),
|
|
3740
|
+
role: String(row.role ?? row.roles?.[0] ?? "tenant_member"),
|
|
3741
|
+
joinedAt: row.joinedAt ?? row.createdAt
|
|
3742
|
+
})));
|
|
3743
|
+
};
|
|
3744
|
+
const submitRename = async (e) => {
|
|
1784
3745
|
e.preventDefault();
|
|
1785
|
-
|
|
3746
|
+
if (!tenantId) return;
|
|
3747
|
+
setRenameSubmitting(true);
|
|
3748
|
+
setError(null);
|
|
1786
3749
|
try {
|
|
1787
|
-
|
|
3750
|
+
const body = {};
|
|
3751
|
+
if (renameValue.trim() && renameValue.trim() !== tenant?.name) body.name = renameValue.trim();
|
|
3752
|
+
if (slugValue.trim() && slugValue.trim() !== tenant?.slug) body.slug = slugValue.trim();
|
|
3753
|
+
if (Object.keys(body).length === 0) {
|
|
3754
|
+
setRenameSubmitting(false);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const res = await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}`, {
|
|
3758
|
+
method: "PATCH",
|
|
3759
|
+
headers: { "Content-Type": "application/json" },
|
|
3760
|
+
credentials: "include",
|
|
3761
|
+
body: JSON.stringify(body)
|
|
3762
|
+
});
|
|
3763
|
+
const t2 = res?.data ?? res;
|
|
3764
|
+
setTenant({ id: t2.id, name: t2.name, slug: t2.slug });
|
|
3765
|
+
setActionMessage("Organization saved.");
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
setError(err instanceof Error ? err.message : "Failed to save");
|
|
3768
|
+
} finally {
|
|
3769
|
+
setRenameSubmitting(false);
|
|
3770
|
+
}
|
|
3771
|
+
};
|
|
3772
|
+
const submitInvite = async (e) => {
|
|
3773
|
+
e.preventDefault();
|
|
3774
|
+
if (!tenantId) return;
|
|
3775
|
+
setInviteSubmitting(true);
|
|
3776
|
+
setError(null);
|
|
3777
|
+
try {
|
|
3778
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/invite`, {
|
|
1788
3779
|
method: "POST",
|
|
1789
3780
|
headers: { "Content-Type": "application/json" },
|
|
1790
|
-
|
|
3781
|
+
credentials: "include",
|
|
3782
|
+
body: JSON.stringify({ email: inviteEmail.trim(), role: inviteRole })
|
|
1791
3783
|
});
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
3784
|
+
setActionMessage(`Invitation sent to ${inviteEmail.trim()}.`);
|
|
3785
|
+
setInviteEmail("");
|
|
3786
|
+
if (activeTab === "invitations") await loadInvites(tenantId);
|
|
1795
3787
|
} catch (err) {
|
|
1796
|
-
|
|
3788
|
+
setError(err instanceof Error ? err.message : "Failed to invite");
|
|
3789
|
+
} finally {
|
|
3790
|
+
setInviteSubmitting(false);
|
|
1797
3791
|
}
|
|
1798
3792
|
};
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
3793
|
+
const changeRole = async (userId, role) => {
|
|
3794
|
+
if (!tenantId) return;
|
|
3795
|
+
try {
|
|
3796
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/${userId}/role`, {
|
|
3797
|
+
method: "PATCH",
|
|
3798
|
+
headers: { "Content-Type": "application/json" },
|
|
3799
|
+
credentials: "include",
|
|
3800
|
+
body: JSON.stringify({ role })
|
|
3801
|
+
});
|
|
3802
|
+
await reloadMembers();
|
|
3803
|
+
setActionMessage("Role updated.");
|
|
3804
|
+
} catch (err) {
|
|
3805
|
+
setError(err instanceof Error ? err.message : "Failed to update role");
|
|
3806
|
+
}
|
|
1802
3807
|
};
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
3808
|
+
const removeMember = async (userId) => {
|
|
3809
|
+
if (!tenantId) return;
|
|
3810
|
+
try {
|
|
3811
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}/users/${userId}`, { method: "DELETE", credentials: "include" });
|
|
3812
|
+
await reloadMembers();
|
|
3813
|
+
setActionMessage("Member removed.");
|
|
3814
|
+
} catch (err) {
|
|
3815
|
+
setError(err instanceof Error ? err.message : "Failed to remove member");
|
|
3816
|
+
}
|
|
3817
|
+
};
|
|
3818
|
+
const resendInvite = async (id) => {
|
|
3819
|
+
if (!tenantId) return;
|
|
3820
|
+
try {
|
|
3821
|
+
await jsonFetch(`${baseUrl}/api/v1/invites/${id}/resend`, { method: "POST", credentials: "include" });
|
|
3822
|
+
setActionMessage("Invitation resent.");
|
|
3823
|
+
await loadInvites(tenantId);
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
setError(err instanceof Error ? err.message : "Failed to resend invitation");
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3828
|
+
const revokeInvite = async (id) => {
|
|
3829
|
+
if (!tenantId) return;
|
|
3830
|
+
try {
|
|
3831
|
+
await jsonFetch(`${baseUrl}/api/v1/invites/${id}/revoke`, { method: "POST", credentials: "include" });
|
|
3832
|
+
setActionMessage("Invitation revoked.");
|
|
3833
|
+
await loadInvites(tenantId);
|
|
3834
|
+
} catch (err) {
|
|
3835
|
+
setError(err instanceof Error ? err.message : "Failed to revoke invitation");
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
const submitDelete = async () => {
|
|
3839
|
+
if (!tenantId || !tenant) return;
|
|
3840
|
+
if (confirmDeleteText !== tenant.slug) {
|
|
3841
|
+
setError("Type the organization slug to confirm deletion.");
|
|
3842
|
+
return;
|
|
3843
|
+
}
|
|
3844
|
+
if (!confirmDeletePassword) {
|
|
3845
|
+
setError("Re-enter your password to confirm deletion.");
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
setDeleteSubmitting(true);
|
|
3849
|
+
setError(null);
|
|
3850
|
+
try {
|
|
3851
|
+
await jsonFetch(`${baseUrl}/api/v1/tenants/${tenantId}`, {
|
|
3852
|
+
method: "DELETE",
|
|
3853
|
+
credentials: "include",
|
|
3854
|
+
headers: { "Content-Type": "application/json" },
|
|
3855
|
+
body: JSON.stringify({ confirmPassword: confirmDeletePassword })
|
|
3856
|
+
});
|
|
3857
|
+
setActionMessage("Organization deleted.");
|
|
3858
|
+
setConfirmDeletePassword("");
|
|
3859
|
+
onDeleted?.(tenantId);
|
|
3860
|
+
} catch (err) {
|
|
3861
|
+
setError(err instanceof Error ? err.message : "Failed to delete organization");
|
|
3862
|
+
} finally {
|
|
3863
|
+
setDeleteSubmitting(false);
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
if (loading) {
|
|
3867
|
+
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" }) });
|
|
3868
|
+
}
|
|
3869
|
+
const tabBtnStyle = (key) => ({
|
|
3870
|
+
background: activeTab === key ? `${branding?.accentColor || "#6366f1"}1a` : "transparent",
|
|
3871
|
+
border: `1px solid ${activeTab === key ? branding?.accentColor || "#6366f1" : "rgba(15,23,42,0.12)"}`,
|
|
3872
|
+
color: branding?.primaryColor || "#0f172a",
|
|
3873
|
+
padding: "6px 12px",
|
|
3874
|
+
borderRadius: 6,
|
|
3875
|
+
cursor: "pointer",
|
|
3876
|
+
fontSize: 12,
|
|
3877
|
+
fontWeight: 500
|
|
3878
|
+
});
|
|
3879
|
+
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: [
|
|
3880
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
3881
|
+
actionMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", "aria-live": "polite", style: { fontSize: 13, color: "#047857", margin: 0 }, children: actionMessage }) : null,
|
|
3882
|
+
/* @__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)(
|
|
3883
|
+
"button",
|
|
3884
|
+
{
|
|
3885
|
+
role: "tab",
|
|
3886
|
+
"aria-selected": activeTab === t2,
|
|
3887
|
+
"aria-controls": `iqauth-org-tab-${t2}`,
|
|
3888
|
+
"data-testid": `tab-org-${t2}`,
|
|
3889
|
+
type: "button",
|
|
3890
|
+
onClick: () => setActiveTab(t2),
|
|
3891
|
+
style: tabBtnStyle(t2),
|
|
3892
|
+
children: t2 === "general" ? "General" : t2 === "members" ? `Members (${members.length})` : t2 === "invitations" ? "Invitations" : "Danger zone"
|
|
3893
|
+
},
|
|
3894
|
+
t2
|
|
3895
|
+
)) }),
|
|
3896
|
+
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: [
|
|
3897
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: "Settings" }),
|
|
3898
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitRename, style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
3899
|
+
/* @__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 }) }),
|
|
3900
|
+
/* @__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 }) }),
|
|
3901
|
+
/* @__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
3902
|
] })
|
|
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
|
-
|
|
3903
|
+
] }) : null,
|
|
3904
|
+
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: [
|
|
3905
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitInvite, style: { display: "flex", gap: 8, flexWrap: "wrap" }, "aria-label": "Invite a new member", children: [
|
|
3906
|
+
/* @__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 }),
|
|
3907
|
+
/* @__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: [
|
|
3908
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_member", children: "tenant_member" }),
|
|
3909
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_admin", children: "tenant_admin" })
|
|
3910
|
+
] }),
|
|
3911
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { "data-testid": "button-org-invite", type: "submit", disabled: inviteSubmitting || !inviteEmail.trim(), children: inviteSubmitting ? "Sending\u2026" : "Send invite" })
|
|
3912
|
+
] }),
|
|
3913
|
+
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: [
|
|
3914
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
3915
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.name || m.email }),
|
|
3916
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.7 }, children: m.email })
|
|
3917
|
+
] }),
|
|
3918
|
+
/* @__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: [
|
|
3919
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_member", children: "tenant_member" }),
|
|
3920
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "tenant_admin", children: "tenant_admin" })
|
|
3921
|
+
] }),
|
|
3922
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3923
|
+
"button",
|
|
3924
|
+
{
|
|
3925
|
+
type: "button",
|
|
3926
|
+
"data-testid": `button-org-member-remove-${m.userId}`,
|
|
3927
|
+
"aria-label": `Remove ${m.email}`,
|
|
3928
|
+
onClick: () => removeMember(m.userId),
|
|
3929
|
+
style: { background: "transparent", border: "1px solid rgba(220,38,38,0.4)", color: "#b91c1c", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
|
|
3930
|
+
children: "Remove"
|
|
3931
|
+
}
|
|
3932
|
+
)
|
|
3933
|
+
] }, m.userId)) })
|
|
3934
|
+
] }) : null,
|
|
3935
|
+
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: [
|
|
3936
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0 }, children: "Pending invitations" }),
|
|
3937
|
+
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: [
|
|
3938
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
3939
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: inv.email }),
|
|
3940
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, opacity: 0.7 }, children: [
|
|
3941
|
+
"role: ",
|
|
3942
|
+
inv.role,
|
|
3943
|
+
inv.expiresAt ? ` \u2022 expires ${new Date(inv.expiresAt).toLocaleDateString()}` : ""
|
|
3944
|
+
] })
|
|
3945
|
+
] }),
|
|
3946
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3947
|
+
"button",
|
|
3948
|
+
{
|
|
3949
|
+
type: "button",
|
|
3950
|
+
"data-testid": `button-org-invite-resend-${inv.id}`,
|
|
3951
|
+
onClick: () => resendInvite(inv.id),
|
|
3952
|
+
style: { background: "transparent", border: "1px solid rgba(15,23,42,0.18)", color: "#0f172a", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
|
|
3953
|
+
children: "Resend"
|
|
3954
|
+
}
|
|
3955
|
+
),
|
|
3956
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3957
|
+
"button",
|
|
3958
|
+
{
|
|
3959
|
+
type: "button",
|
|
3960
|
+
"data-testid": `button-org-invite-revoke-${inv.id}`,
|
|
3961
|
+
onClick: () => revokeInvite(inv.id),
|
|
3962
|
+
style: { background: "transparent", border: "1px solid rgba(220,38,38,0.4)", color: "#b91c1c", borderRadius: 4, padding: "4px 10px", cursor: "pointer", fontSize: 12 },
|
|
3963
|
+
children: "Revoke"
|
|
3964
|
+
}
|
|
3965
|
+
)
|
|
3966
|
+
] }, inv.id)) })
|
|
3967
|
+
] }) : null,
|
|
3968
|
+
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: [
|
|
3969
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: 14, fontWeight: 600, margin: 0, color: "#b91c1c" }, children: "Delete organization" }),
|
|
3970
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 12, opacity: 0.85, margin: 0 }, children: [
|
|
3971
|
+
"This permanently deletes ",
|
|
3972
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: tenant?.name }),
|
|
3973
|
+
" and all of its members, roles, and audit history. To confirm, type the slug ",
|
|
3974
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: tenant?.slug }),
|
|
3975
|
+
" below."
|
|
3976
|
+
] }),
|
|
3977
|
+
/* @__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) }) }),
|
|
3978
|
+
/* @__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) }) }),
|
|
3979
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3980
|
+
"button",
|
|
3981
|
+
{
|
|
3982
|
+
type: "button",
|
|
3983
|
+
"data-testid": "button-org-delete",
|
|
3984
|
+
onClick: submitDelete,
|
|
3985
|
+
disabled: deleteSubmitting || confirmDeleteText !== tenant?.slug || !confirmDeletePassword,
|
|
3986
|
+
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 },
|
|
3987
|
+
children: deleteSubmitting ? "Deleting\u2026" : "Permanently delete organization"
|
|
3988
|
+
}
|
|
3989
|
+
)
|
|
3990
|
+
] }) : null
|
|
3991
|
+
] }) });
|
|
1837
3992
|
}
|
|
1838
|
-
function
|
|
3993
|
+
function OrganizationList({ iqAuthBaseUrl, onSelect, showCreate = true, createRedirectUrl, appearance, className }) {
|
|
3994
|
+
const [showCreateForm, setShowCreateForm] = (0, import_react.useState)(false);
|
|
3995
|
+
const reloadList = () => {
|
|
3996
|
+
setShowCreateForm(false);
|
|
3997
|
+
if (typeof window !== "undefined") setTimeout(() => window.location.reload(), 50);
|
|
3998
|
+
};
|
|
1839
3999
|
const branding = useResolvedSdkBranding(iqAuthBaseUrl);
|
|
4000
|
+
const baseUrl = iqAuthBaseUrl.replace(/\/$/, "");
|
|
1840
4001
|
const accent = branding?.accentColor || "#6366f1";
|
|
1841
4002
|
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
1842
4003
|
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
1843
|
-
const [
|
|
4004
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
4005
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
1844
4006
|
(0, import_react.useEffect)(() => {
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
4007
|
+
let cancelled = false;
|
|
4008
|
+
Promise.all([
|
|
4009
|
+
fetch(`${baseUrl}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()),
|
|
4010
|
+
fetch(`${baseUrl}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json())
|
|
4011
|
+
]).then(([me, mems]) => {
|
|
4012
|
+
if (cancelled) return;
|
|
4013
|
+
setActiveTenantId(me?.data?.tenantId || null);
|
|
4014
|
+
setMemberships(mems?.data?.memberships || mems?.data || []);
|
|
4015
|
+
}).catch((err) => {
|
|
4016
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load organizations");
|
|
4017
|
+
}).finally(() => {
|
|
4018
|
+
if (!cancelled) setLoading(false);
|
|
1850
4019
|
});
|
|
1851
|
-
|
|
1852
|
-
|
|
4020
|
+
return () => {
|
|
4021
|
+
cancelled = true;
|
|
4022
|
+
};
|
|
4023
|
+
}, [baseUrl]);
|
|
4024
|
+
const select = async (tenantId) => {
|
|
4025
|
+
if (tenantId === activeTenantId) {
|
|
4026
|
+
onSelect?.(tenantId);
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
1853
4029
|
try {
|
|
1854
|
-
await jsonFetch(`${
|
|
4030
|
+
await jsonFetch(`${baseUrl}/api/v1/auth/select-tenant`, {
|
|
1855
4031
|
method: "POST",
|
|
1856
4032
|
headers: { "Content-Type": "application/json" },
|
|
4033
|
+
credentials: "include",
|
|
1857
4034
|
body: JSON.stringify({ tenantId })
|
|
1858
4035
|
});
|
|
1859
4036
|
setActiveTenantId(tenantId);
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
4037
|
+
onSelect?.(tenantId);
|
|
4038
|
+
} catch (err) {
|
|
4039
|
+
setError(err instanceof Error ? err.message : "Failed to switch organization");
|
|
1863
4040
|
}
|
|
1864
4041
|
};
|
|
1865
|
-
|
|
4042
|
+
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: [
|
|
4043
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
4044
|
+
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) => {
|
|
4045
|
+
const active = m.tenantId === activeTenantId;
|
|
4046
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
4047
|
+
"button",
|
|
4048
|
+
{
|
|
4049
|
+
type: "button",
|
|
4050
|
+
"data-testid": `button-org-list-${m.tenantId}`,
|
|
4051
|
+
onClick: () => select(m.tenantId),
|
|
4052
|
+
style: {
|
|
4053
|
+
display: "block",
|
|
4054
|
+
width: "100%",
|
|
4055
|
+
textAlign: "left",
|
|
4056
|
+
padding: "10px 12px",
|
|
4057
|
+
borderRadius: 6,
|
|
4058
|
+
cursor: "pointer",
|
|
4059
|
+
fontSize: 13,
|
|
4060
|
+
background: active ? `${accent}1a` : "transparent",
|
|
4061
|
+
border: `1px solid ${active ? accent : "rgba(15,23,42,0.12)"}`,
|
|
4062
|
+
color: branding?.primaryColor || "#0f172a"
|
|
4063
|
+
},
|
|
4064
|
+
children: [
|
|
4065
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
4066
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, opacity: 0.7 }, children: [
|
|
4067
|
+
(m.roles || []).join(", ") || "\u2014",
|
|
4068
|
+
active ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { marginLeft: 8, color: accent, fontWeight: 600 }, children: "active" }) : null
|
|
4069
|
+
] })
|
|
4070
|
+
]
|
|
4071
|
+
}
|
|
4072
|
+
) }, m.tenantId);
|
|
4073
|
+
}) }),
|
|
4074
|
+
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: [
|
|
4075
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4076
|
+
CreateOrganization,
|
|
4077
|
+
{
|
|
4078
|
+
iqAuthBaseUrl,
|
|
4079
|
+
unstyled: true,
|
|
4080
|
+
appearance,
|
|
4081
|
+
onCreated: (t2) => {
|
|
4082
|
+
onSelect?.(t2.id);
|
|
4083
|
+
reloadList();
|
|
4084
|
+
},
|
|
4085
|
+
redirectUrl: createRedirectUrl
|
|
4086
|
+
}
|
|
4087
|
+
),
|
|
4088
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4089
|
+
"button",
|
|
4090
|
+
{
|
|
4091
|
+
type: "button",
|
|
4092
|
+
"data-testid": "button-org-list-create-cancel",
|
|
4093
|
+
onClick: () => setShowCreateForm(false),
|
|
4094
|
+
style: { marginTop: 8, background: "transparent", border: "none", color: branding?.accentColor || "#6366f1", cursor: "pointer", fontSize: 12, padding: 0 },
|
|
4095
|
+
children: "Cancel"
|
|
4096
|
+
}
|
|
4097
|
+
)
|
|
4098
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4099
|
+
"button",
|
|
4100
|
+
{
|
|
4101
|
+
type: "button",
|
|
4102
|
+
"data-testid": "button-org-list-create",
|
|
4103
|
+
onClick: () => setShowCreateForm(true),
|
|
4104
|
+
style: { background: "transparent", border: `1px dashed ${accent}`, color: branding?.primaryColor || "#0f172a", padding: "10px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13, width: "100%" },
|
|
4105
|
+
children: "+ Create new organization"
|
|
4106
|
+
}
|
|
4107
|
+
) }) : null
|
|
4108
|
+
] }) });
|
|
4109
|
+
}
|
|
4110
|
+
function Waitlist({ iqAuthBaseUrl, appKey, appId, title, subtitle, successMessage, appearance, className }) {
|
|
4111
|
+
const branding = useResolvedSdkBranding(iqAuthBaseUrl, appId);
|
|
4112
|
+
const [email, setEmail] = (0, import_react.useState)("");
|
|
4113
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
4114
|
+
const [organizationName, setOrganizationName] = (0, import_react.useState)("");
|
|
4115
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
4116
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4117
|
+
const [submitted, setSubmitted] = (0, import_react.useState)(null);
|
|
4118
|
+
const submit = async (e) => {
|
|
4119
|
+
e.preventDefault();
|
|
4120
|
+
setSubmitting(true);
|
|
4121
|
+
setError(null);
|
|
4122
|
+
try {
|
|
4123
|
+
const res = await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/waitlist`, {
|
|
4124
|
+
method: "POST",
|
|
4125
|
+
headers: { "Content-Type": "application/json" },
|
|
4126
|
+
body: JSON.stringify({
|
|
4127
|
+
email: email.trim().toLowerCase(),
|
|
4128
|
+
name: name.trim() || void 0,
|
|
4129
|
+
organizationName: organizationName.trim() || void 0,
|
|
4130
|
+
appKey: appKey || void 0,
|
|
4131
|
+
appId: appId || void 0
|
|
4132
|
+
})
|
|
4133
|
+
});
|
|
4134
|
+
const data = res?.data ?? res;
|
|
4135
|
+
setSubmitted({ duplicate: !!data?.duplicate });
|
|
4136
|
+
} catch (err) {
|
|
4137
|
+
setError(err instanceof Error ? err.message : "Failed to join the waitlist");
|
|
4138
|
+
} finally {
|
|
4139
|
+
setSubmitting(false);
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
if (submitted) {
|
|
4143
|
+
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.") }) });
|
|
4144
|
+
}
|
|
4145
|
+
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: [
|
|
4146
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
4147
|
+
/* @__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) }) }),
|
|
4148
|
+
/* @__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) }) }),
|
|
4149
|
+
/* @__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) }) }),
|
|
4150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { "data-testid": "button-waitlist-submit", type: "submit", disabled: submitting || !email, children: submitting ? "Joining\u2026" : "Join the waitlist" })
|
|
4151
|
+
] }) });
|
|
4152
|
+
}
|
|
4153
|
+
function usePasswordlessOptions(override) {
|
|
4154
|
+
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
4155
|
+
const baseFromCtx = ctx?.manager?.issuerUrl;
|
|
4156
|
+
const iqAuthBaseUrl = override?.iqAuthBaseUrl || baseFromCtx || (typeof window !== "undefined" ? window.location.origin : "");
|
|
4157
|
+
return { iqAuthBaseUrl, cookieSession: override?.cookieSession ?? true };
|
|
4158
|
+
}
|
|
4159
|
+
function useMagicLink(override) {
|
|
4160
|
+
const opts = usePasswordlessOptions(override);
|
|
4161
|
+
const [sent, setSent] = (0, import_react.useState)(false);
|
|
4162
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
4163
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4164
|
+
const request = (0, import_react.useCallback)(async (input) => {
|
|
4165
|
+
setBusy(true);
|
|
4166
|
+
setError(null);
|
|
4167
|
+
setSent(false);
|
|
4168
|
+
try {
|
|
4169
|
+
await requestMagicLink(opts, input);
|
|
4170
|
+
setSent(true);
|
|
4171
|
+
} catch (e) {
|
|
4172
|
+
setError(e instanceof Error ? e.message : "Magic link request failed");
|
|
4173
|
+
} finally {
|
|
4174
|
+
setBusy(false);
|
|
4175
|
+
}
|
|
4176
|
+
}, [opts.iqAuthBaseUrl, opts.cookieSession]);
|
|
4177
|
+
return { request, sent, busy, error };
|
|
4178
|
+
}
|
|
4179
|
+
function usePasskey(override) {
|
|
4180
|
+
const opts = usePasswordlessOptions(override);
|
|
4181
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
4182
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4183
|
+
const signIn2 = (0, import_react.useCallback)(async (input = {}) => {
|
|
4184
|
+
setBusy(true);
|
|
4185
|
+
setError(null);
|
|
4186
|
+
try {
|
|
4187
|
+
return await signInWithPasskey(opts, input);
|
|
4188
|
+
} catch (e) {
|
|
4189
|
+
const msg = e instanceof Error ? e.message : "Passkey sign-in failed";
|
|
4190
|
+
setError(msg);
|
|
4191
|
+
throw e;
|
|
4192
|
+
} finally {
|
|
4193
|
+
setBusy(false);
|
|
4194
|
+
}
|
|
4195
|
+
}, [opts.iqAuthBaseUrl]);
|
|
4196
|
+
const enroll = (0, import_react.useCallback)(async (name) => {
|
|
4197
|
+
setBusy(true);
|
|
4198
|
+
setError(null);
|
|
4199
|
+
try {
|
|
4200
|
+
return await enrollPasskey(opts, name);
|
|
4201
|
+
} catch (e) {
|
|
4202
|
+
const msg = e instanceof Error ? e.message : "Passkey enrollment failed";
|
|
4203
|
+
setError(msg);
|
|
4204
|
+
throw e;
|
|
4205
|
+
} finally {
|
|
4206
|
+
setBusy(false);
|
|
4207
|
+
}
|
|
4208
|
+
}, [opts.iqAuthBaseUrl]);
|
|
4209
|
+
return { signIn: signIn2, enroll, busy, error };
|
|
4210
|
+
}
|
|
4211
|
+
function useLinkedIdentities(override) {
|
|
4212
|
+
const opts = usePasswordlessOptions(override);
|
|
4213
|
+
const [identities, setIdentities] = (0, import_react.useState)([]);
|
|
4214
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
4215
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
4216
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
4217
|
+
setLoading(true);
|
|
4218
|
+
setError(null);
|
|
4219
|
+
try {
|
|
4220
|
+
setIdentities(await listLinkedIdentities(opts));
|
|
4221
|
+
} catch (e) {
|
|
4222
|
+
setError(e instanceof Error ? e.message : "Failed to load identities");
|
|
4223
|
+
} finally {
|
|
4224
|
+
setLoading(false);
|
|
4225
|
+
}
|
|
4226
|
+
}, [opts.iqAuthBaseUrl]);
|
|
4227
|
+
const link = (0, import_react.useCallback)(async (input) => {
|
|
4228
|
+
await linkProvider(opts, input);
|
|
4229
|
+
await refresh();
|
|
4230
|
+
}, [opts.iqAuthBaseUrl, refresh]);
|
|
4231
|
+
const unlink = (0, import_react.useCallback)(async (provider, password) => {
|
|
4232
|
+
await unlinkProvider(opts, { provider, reauth: { password } });
|
|
4233
|
+
await refresh();
|
|
4234
|
+
}, [opts.iqAuthBaseUrl, refresh]);
|
|
4235
|
+
(0, import_react.useEffect)(() => {
|
|
4236
|
+
refresh();
|
|
4237
|
+
}, [refresh]);
|
|
4238
|
+
return { identities, loading, error, refresh, link, unlink };
|
|
4239
|
+
}
|
|
4240
|
+
function MagicLinkSignInForm(props) {
|
|
4241
|
+
const { request, sent, busy, error } = useMagicLink(props);
|
|
4242
|
+
const [email, setEmail] = (0, import_react.useState)("");
|
|
1866
4243
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1867
|
-
"
|
|
4244
|
+
"form",
|
|
1868
4245
|
{
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
4246
|
+
"data-testid": "form-magic-link",
|
|
4247
|
+
className: props.className,
|
|
4248
|
+
onSubmit: (e) => {
|
|
4249
|
+
e.preventDefault();
|
|
4250
|
+
if (email) void request({ email, appId: props.appId, redirectUri: props.redirectUri });
|
|
4251
|
+
},
|
|
4252
|
+
style: { display: "flex", flexDirection: "column", gap: 8 },
|
|
1873
4253
|
children: [
|
|
1874
4254
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1875
|
-
"
|
|
4255
|
+
"input",
|
|
1876
4256
|
{
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
4257
|
+
"data-testid": "input-magic-link-email",
|
|
4258
|
+
type: "email",
|
|
4259
|
+
required: true,
|
|
4260
|
+
value: email,
|
|
4261
|
+
placeholder: props.placeholder ?? "you@example.com",
|
|
4262
|
+
onChange: (e) => setEmail(e.target.value),
|
|
4263
|
+
style: { padding: 8, border: "1px solid rgba(15,23,42,0.15)", borderRadius: 6 }
|
|
1883
4264
|
}
|
|
1884
4265
|
),
|
|
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
|
|
4266
|
+
/* @__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" }),
|
|
4267
|
+
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,
|
|
4268
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-magic-link-error", style: { fontSize: 12, color: "#b91c1c" }, children: error }) : null
|
|
1921
4269
|
]
|
|
1922
4270
|
}
|
|
1923
4271
|
);
|
|
1924
4272
|
}
|
|
4273
|
+
function PasskeySignInButton({ email, className, children, ...rest }) {
|
|
4274
|
+
const { signIn: signIn2, busy, error } = usePasskey(rest);
|
|
4275
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
|
|
4276
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4277
|
+
"button",
|
|
4278
|
+
{
|
|
4279
|
+
"data-testid": "button-passkey-signin",
|
|
4280
|
+
disabled: busy,
|
|
4281
|
+
onClick: () => void signIn2({ email }).catch(() => {
|
|
4282
|
+
}),
|
|
4283
|
+
style: { padding: "8px 12px", borderRadius: 6, border: "1px solid rgba(15,23,42,0.15)", background: "transparent", cursor: "pointer" },
|
|
4284
|
+
children: busy ? "Verifying\u2026" : children ?? "Sign in with a passkey"
|
|
4285
|
+
}
|
|
4286
|
+
),
|
|
4287
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "text-passkey-error", style: { fontSize: 12, color: "#b91c1c", marginTop: 4 }, children: error }) : null
|
|
4288
|
+
] });
|
|
4289
|
+
}
|
|
4290
|
+
function LinkedAccounts({ className, onChange, ...rest }) {
|
|
4291
|
+
const { identities, loading, error, unlink } = useLinkedIdentities(rest);
|
|
4292
|
+
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: [
|
|
4293
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
4294
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { textTransform: "capitalize" }, children: i.provider }),
|
|
4295
|
+
" ",
|
|
4296
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.7 }, children: i.label || i.providerUserId || "" })
|
|
4297
|
+
] }),
|
|
4298
|
+
i.canUnlink ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
4299
|
+
"button",
|
|
4300
|
+
{
|
|
4301
|
+
"data-testid": `button-unlink-${i.provider}`,
|
|
4302
|
+
onClick: async () => {
|
|
4303
|
+
const pw = window.prompt("Confirm your password to unlink this identity") || void 0;
|
|
4304
|
+
try {
|
|
4305
|
+
await unlink(i.provider, pw);
|
|
4306
|
+
onChange?.();
|
|
4307
|
+
} catch {
|
|
4308
|
+
}
|
|
4309
|
+
},
|
|
4310
|
+
children: "Unlink"
|
|
4311
|
+
}
|
|
4312
|
+
) : null
|
|
4313
|
+
] }, i.id)) }) });
|
|
4314
|
+
}
|
|
1925
4315
|
var __version__ = "phase-bc-1.0.0";
|
|
1926
4316
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1927
4317
|
0 && (module.exports = {
|
|
1928
4318
|
AuthCallback,
|
|
4319
|
+
CreateOrganization,
|
|
1929
4320
|
IQAuthLoaded,
|
|
1930
4321
|
IQAuthLoading,
|
|
1931
4322
|
IQAuthProvider,
|
|
4323
|
+
IQAuthReturnToBouncer,
|
|
4324
|
+
ImpersonationBanner,
|
|
4325
|
+
LinkedAccounts,
|
|
4326
|
+
MagicLinkSignInForm,
|
|
4327
|
+
MultisessionAppSupport,
|
|
4328
|
+
OrganizationList,
|
|
4329
|
+
OrganizationProfile,
|
|
1932
4330
|
OrganizationSwitcher,
|
|
4331
|
+
PasskeySignInButton,
|
|
4332
|
+
Protect,
|
|
1933
4333
|
RedirectToSignIn,
|
|
4334
|
+
RedirectToSignedIn,
|
|
1934
4335
|
SignIn,
|
|
1935
4336
|
SignUp,
|
|
1936
4337
|
SignedIn,
|
|
1937
4338
|
SignedOut,
|
|
1938
4339
|
UserButton,
|
|
1939
4340
|
UserProfile,
|
|
4341
|
+
Waitlist,
|
|
1940
4342
|
__version__,
|
|
4343
|
+
isReturnToAllowed,
|
|
1941
4344
|
isSilentSsoEligible,
|
|
4345
|
+
preflightReturnTo,
|
|
4346
|
+
revokeSession,
|
|
1942
4347
|
sanitizeBrandCss,
|
|
4348
|
+
sanitizeReturnTo,
|
|
4349
|
+
slugify,
|
|
4350
|
+
useAccountList,
|
|
4351
|
+
useAccountSwitcher,
|
|
1943
4352
|
useAuth,
|
|
1944
4353
|
useAuthFetch,
|
|
1945
4354
|
useIQAuthSignInContext,
|
|
4355
|
+
useImpersonation,
|
|
4356
|
+
useLinkedIdentities,
|
|
4357
|
+
useLocale,
|
|
4358
|
+
useMagicLink,
|
|
1946
4359
|
useOrganization,
|
|
4360
|
+
usePasskey,
|
|
1947
4361
|
useResolvedSdkBranding,
|
|
4362
|
+
useReturnTo,
|
|
4363
|
+
useReverification,
|
|
1948
4364
|
useSession,
|
|
4365
|
+
useSessionList,
|
|
4366
|
+
useT,
|
|
1949
4367
|
useUser
|
|
1950
4368
|
});
|