@iqauth/sdk 2.0.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/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/browser-session.d.mts +12 -0
- package/dist/browser-session.d.ts +12 -0
- package/dist/browser-session.js +1812 -0
- package/dist/browser-session.mjs +28 -0
- package/dist/browser.d.mts +46 -0
- package/dist/browser.d.ts +46 -0
- package/dist/browser.js +768 -0
- package/dist/browser.mjs +47 -0
- package/dist/chunk-5HF3OBNO.mjs +189 -0
- package/dist/chunk-5WFR6Y33.mjs +59 -0
- package/dist/chunk-6I6RM4MN.mjs +51 -0
- package/dist/chunk-73R6BEGO.mjs +176 -0
- package/dist/chunk-E46DKOVI.mjs +632 -0
- package/dist/chunk-JQWYIIIS.mjs +1740 -0
- package/dist/chunk-X3K3WOBR.mjs +64 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +581 -0
- package/dist/cli/index.mjs +57 -0
- package/dist/client-C1DXfB8Z.d.mts +911 -0
- package/dist/client-CggvJmmm.d.ts +911 -0
- package/dist/dev-FUTJZSWN.mjs +56 -0
- package/dist/doctor-OHJRZBBT.mjs +89 -0
- package/dist/errors-CDdl24MP.d.mts +52 -0
- package/dist/errors-CDdl24MP.d.ts +52 -0
- package/dist/express-BKAXB5Nl.d.ts +61 -0
- package/dist/express-CpfyYTmw.d.mts +61 -0
- package/dist/express.d.mts +45 -0
- package/dist/express.d.ts +45 -0
- package/dist/express.js +2252 -0
- package/dist/express.mjs +122 -0
- package/dist/fastify.d.mts +23 -0
- package/dist/fastify.d.ts +23 -0
- package/dist/fastify.js +2062 -0
- package/dist/fastify.mjs +118 -0
- package/dist/hono.d.mts +22 -0
- package/dist/hono.d.ts +22 -0
- package/dist/hono.js +2051 -0
- package/dist/hono.mjs +107 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2070 -0
- package/dist/index.mjs +83 -0
- package/dist/init-LLCSQGNL.mjs +198 -0
- package/dist/keys-NLWFAOEM.mjs +63 -0
- package/dist/mobile.d.mts +11 -0
- package/dist/mobile.d.ts +11 -0
- package/dist/mobile.js +1809 -0
- package/dist/mobile.mjs +25 -0
- package/dist/next.d.mts +37 -0
- package/dist/next.d.ts +37 -0
- package/dist/next.js +2078 -0
- package/dist/next.mjs +130 -0
- package/dist/publishableKey-B5DIK81A.d.mts +24 -0
- package/dist/publishableKey-B5DIK81A.d.ts +24 -0
- package/dist/react.d.mts +196 -0
- package/dist/react.d.ts +196 -0
- package/dist/react.js +1457 -0
- package/dist/react.mjs +787 -0
- package/dist/server/handlers.d.mts +96 -0
- package/dist/server/handlers.d.ts +96 -0
- package/dist/server/handlers.js +243 -0
- package/dist/server/handlers.mjs +14 -0
- package/dist/server.d.mts +14 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +2195 -0
- package/dist/server.mjs +47 -0
- package/dist/service.d.mts +11 -0
- package/dist/service.d.ts +11 -0
- package/dist/service.js +1809 -0
- package/dist/service.mjs +25 -0
- package/dist/signIn-C8f6qVjD.d.mts +238 -0
- package/dist/signIn-Cy2lbEXb.d.ts +238 -0
- package/dist/types-Cxl3bQHt.d.mts +900 -0
- package/dist/types-Cxl3bQHt.d.ts +900 -0
- package/docs/APP_INTEGRATION_MATRIX.md +59 -0
- package/docs/BROWSER_SESSION_MIGRATION.md +69 -0
- package/docs/FRESH_IMPLEMENTATION_GUIDE.md +188 -0
- package/docs/TARBALL_RELEASE_WORKFLOW.md +98 -0
- package/docs/V1_TO_V2_UPGRADE_GUIDE.md +318 -0
- package/docs/guides/api-keys.md +130 -0
- package/docs/guides/app-registration.md +149 -0
- package/docs/guides/auth-flows.md +168 -0
- package/docs/guides/branding.md +160 -0
- package/docs/guides/entitlements.md +115 -0
- package/docs/guides/entity-hierarchy.md +200 -0
- package/docs/guides/error-handling.md +251 -0
- package/docs/guides/gdpr-compliance.md +123 -0
- package/docs/guides/invitations.md +143 -0
- package/docs/guides/mfa-enrollment.md +170 -0
- package/docs/guides/middleware-reference.md +205 -0
- package/docs/guides/mobile-native.md +110 -0
- package/docs/guides/roles-and-permissions.md +220 -0
- package/docs/guides/scoped-authorization.md +247 -0
- package/docs/guides/server-platform-integration.md +52 -0
- package/docs/guides/service-automation-integration.md +36 -0
- package/docs/guides/session-management.md +97 -0
- package/docs/guides/tenant-management.md +216 -0
- package/docs/guides/token-verification.md +178 -0
- package/docs/guides/user-management.md +184 -0
- package/docs/guides/webhooks.md +136 -0
- package/docs/integration-prompts/README.md +20 -0
- package/docs/integration-prompts/first-party-browser-app.md +29 -0
- package/docs/integration-prompts/install-from-tarball.md +41 -0
- package/docs/integration-prompts/migrate-from-local-packages-source.md +57 -0
- package/docs/integration-prompts/native-mobile-app.md +24 -0
- package/docs/integration-prompts/server-platform-app.md +20 -0
- package/docs/integration-prompts/service-automation-app.md +20 -0
- package/package.json +115 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
AuthCallback: () => AuthCallback,
|
|
24
|
+
IQAuthProvider: () => IQAuthProvider,
|
|
25
|
+
OrganizationSwitcher: () => OrganizationSwitcher,
|
|
26
|
+
RedirectToSignIn: () => RedirectToSignIn,
|
|
27
|
+
SignIn: () => SignIn,
|
|
28
|
+
SignUp: () => SignUp,
|
|
29
|
+
SignedIn: () => SignedIn,
|
|
30
|
+
SignedOut: () => SignedOut,
|
|
31
|
+
UserButton: () => UserButton,
|
|
32
|
+
UserProfile: () => UserProfile,
|
|
33
|
+
__version__: () => __version__,
|
|
34
|
+
useAuth: () => useAuth,
|
|
35
|
+
useAuthFetch: () => useAuthFetch,
|
|
36
|
+
useIQAuthSignInContext: () => useIQAuthSignInContext,
|
|
37
|
+
useOrganization: () => useOrganization,
|
|
38
|
+
useSession: () => useSession,
|
|
39
|
+
useUser: () => useUser
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(react_exports);
|
|
42
|
+
|
|
43
|
+
// src/react/index.tsx
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
|
|
46
|
+
// src/errors.ts
|
|
47
|
+
var IQAuthError = class extends Error {
|
|
48
|
+
constructor(code, message, status, raw) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "IQAuthError";
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.status = status;
|
|
53
|
+
this.raw = raw;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/publishableKey.ts
|
|
58
|
+
function b64urlDecode(input) {
|
|
59
|
+
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
60
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
61
|
+
if (typeof atob === "function") {
|
|
62
|
+
const bin = atob(normalized);
|
|
63
|
+
const bytes = new Uint8Array(bin.length);
|
|
64
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
65
|
+
return new TextDecoder().decode(bytes);
|
|
66
|
+
}
|
|
67
|
+
const { Buffer: Buffer2 } = require("buffer");
|
|
68
|
+
return Buffer2.from(normalized, "base64").toString("utf8");
|
|
69
|
+
}
|
|
70
|
+
function parsePublishableKey(raw) {
|
|
71
|
+
if (typeof raw !== "string") return null;
|
|
72
|
+
const m = raw.match(/^pk_(test|live)_([A-Za-z0-9_-]+)$/);
|
|
73
|
+
if (!m) return null;
|
|
74
|
+
try {
|
|
75
|
+
const json = JSON.parse(b64urlDecode(m[2]));
|
|
76
|
+
if (!json || typeof json !== "object") return null;
|
|
77
|
+
if (typeof json.iss !== "string" || typeof json.appId !== "string" || typeof json.tenantId !== "string" || typeof json.kid !== "string") {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return { mode: m[1], iss: json.iss, appId: json.appId, tenantId: json.tenantId, kid: json.kid, raw };
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/browser/storage.ts
|
|
87
|
+
var REFRESH_COOKIE = "iqauth_rt";
|
|
88
|
+
var PKCE_STORAGE_PREFIX = "iqauth.pkce.";
|
|
89
|
+
function isBrowser() {
|
|
90
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
91
|
+
}
|
|
92
|
+
function setCookie(name, value, opts = {}) {
|
|
93
|
+
if (!isBrowser()) return;
|
|
94
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
95
|
+
parts.push(`Path=${opts.path ?? "/"}`);
|
|
96
|
+
if (opts.maxAgeSeconds !== void 0) parts.push(`Max-Age=${opts.maxAgeSeconds}`);
|
|
97
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
98
|
+
if (opts.secure ?? location.protocol === "https:") parts.push("Secure");
|
|
99
|
+
parts.push(`SameSite=${opts.sameSite ?? "lax"}`);
|
|
100
|
+
document.cookie = parts.join("; ");
|
|
101
|
+
}
|
|
102
|
+
function getCookie(name) {
|
|
103
|
+
if (!isBrowser()) return null;
|
|
104
|
+
const target = `${name}=`;
|
|
105
|
+
const segments = document.cookie ? document.cookie.split(";") : [];
|
|
106
|
+
for (const seg of segments) {
|
|
107
|
+
const trimmed = seg.trim();
|
|
108
|
+
if (trimmed.startsWith(target)) {
|
|
109
|
+
try {
|
|
110
|
+
return decodeURIComponent(trimmed.slice(target.length));
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function clearCookie(name, opts = {}) {
|
|
119
|
+
setCookie(name, "", { ...opts, maxAgeSeconds: 0 });
|
|
120
|
+
}
|
|
121
|
+
function savePkce(record) {
|
|
122
|
+
if (!isBrowser()) return;
|
|
123
|
+
try {
|
|
124
|
+
sessionStorage.setItem(PKCE_STORAGE_PREFIX + record.state, JSON.stringify(record));
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function loadPkce(state) {
|
|
129
|
+
if (!isBrowser()) return null;
|
|
130
|
+
try {
|
|
131
|
+
const raw = sessionStorage.getItem(PKCE_STORAGE_PREFIX + state);
|
|
132
|
+
if (!raw) return null;
|
|
133
|
+
return JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function clearPkce(state) {
|
|
139
|
+
if (!isBrowser()) return;
|
|
140
|
+
try {
|
|
141
|
+
sessionStorage.removeItem(PKCE_STORAGE_PREFIX + state);
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/browser/sessionManager.ts
|
|
147
|
+
var DEFAULT_REFRESH_PATH = "/api/v1/auth/refresh";
|
|
148
|
+
var DEFAULT_USERINFO_PATH = "/api/v1/auth/me";
|
|
149
|
+
function decodeClaims(token) {
|
|
150
|
+
try {
|
|
151
|
+
const parts = token.split(".");
|
|
152
|
+
if (parts.length !== 3) return null;
|
|
153
|
+
const json = typeof atob === "function" ? decodeURIComponent(
|
|
154
|
+
atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
155
|
+
) : Buffer.from(parts[1], "base64url").toString("utf8");
|
|
156
|
+
return JSON.parse(json);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function claimsToSessionUser(claims) {
|
|
162
|
+
if (!claims) return null;
|
|
163
|
+
return {
|
|
164
|
+
sub: claims.sub,
|
|
165
|
+
email: claims.email,
|
|
166
|
+
name: claims.name,
|
|
167
|
+
tenantId: claims.tenantId,
|
|
168
|
+
vendorId: claims.vendorId,
|
|
169
|
+
roles: claims.roles ?? [],
|
|
170
|
+
entitlements: claims.entitlements ?? []
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
var EMPTY = {
|
|
174
|
+
status: "loading",
|
|
175
|
+
accessToken: null,
|
|
176
|
+
user: null,
|
|
177
|
+
claims: null,
|
|
178
|
+
tenantId: null,
|
|
179
|
+
error: null,
|
|
180
|
+
version: 0
|
|
181
|
+
};
|
|
182
|
+
function defaultCookieStore() {
|
|
183
|
+
return {
|
|
184
|
+
read: () => getCookie(REFRESH_COOKIE),
|
|
185
|
+
write: (token) => setCookie(REFRESH_COOKIE, token, { maxAgeSeconds: 60 * 60 * 24 * 30 }),
|
|
186
|
+
clear: () => clearCookie(REFRESH_COOKIE)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
var NO_OP_STORE = {
|
|
190
|
+
read: () => null,
|
|
191
|
+
write: () => void 0,
|
|
192
|
+
clear: () => void 0
|
|
193
|
+
};
|
|
194
|
+
var SessionManager = class {
|
|
195
|
+
constructor(options) {
|
|
196
|
+
this.snapshot = { ...EMPTY };
|
|
197
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
198
|
+
this.refreshPromise = null;
|
|
199
|
+
this.channel = null;
|
|
200
|
+
this.proactiveTimer = null;
|
|
201
|
+
this.bootstrapped = false;
|
|
202
|
+
/** Pending refresh awaited by other tabs after a `refresh:claim` from us. */
|
|
203
|
+
this.remoteRefreshWaiters = [];
|
|
204
|
+
/** Active claims by other tabs (keyed by source tabId). */
|
|
205
|
+
this.foreignClaim = null;
|
|
206
|
+
const parsed = parsePublishableKey(options.publishableKey);
|
|
207
|
+
if (!parsed) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Invalid IQAuth publishable key. Expected pk_test_\u2026 or pk_live_\u2026 (got ${options.publishableKey?.slice(0, 12) ?? "<empty>"}\u2026).`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
this.key = parsed;
|
|
213
|
+
const inferred = options.issuer ?? (parsed.iss.startsWith("http") ? parsed.iss : `https://${parsed.iss}`);
|
|
214
|
+
this.issuer = inferred.replace(/\/+$/, "");
|
|
215
|
+
this.refreshPath = options.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
216
|
+
this.userinfoPath = options.userinfoPath ?? DEFAULT_USERINFO_PATH;
|
|
217
|
+
this.useCookies = options.useCookies ?? true;
|
|
218
|
+
this.proactiveRefresh = options.proactiveRefresh ?? true;
|
|
219
|
+
this.tokenStore = options.tokenStore ?? (this.useCookies ? defaultCookieStore() : NO_OP_STORE);
|
|
220
|
+
this.crossTabLockTimeoutMs = options.crossTabLockTimeoutMs ?? 4e3;
|
|
221
|
+
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => {
|
|
222
|
+
throw new Error("global fetch is not available; pass fetchImpl");
|
|
223
|
+
}));
|
|
224
|
+
this.tabId = Math.random().toString(36).slice(2);
|
|
225
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
226
|
+
const name = options.channelName ?? `iqauth.${parsed.appId}`;
|
|
227
|
+
try {
|
|
228
|
+
this.channel = new BroadcastChannel(name);
|
|
229
|
+
this.channel.onmessage = (ev) => this.onBroadcast(ev.data);
|
|
230
|
+
} catch {
|
|
231
|
+
this.channel = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
get publishableKey() {
|
|
236
|
+
return this.key;
|
|
237
|
+
}
|
|
238
|
+
get appKey() {
|
|
239
|
+
return this.key.appId;
|
|
240
|
+
}
|
|
241
|
+
get tenantIdFromKey() {
|
|
242
|
+
return this.key.tenantId;
|
|
243
|
+
}
|
|
244
|
+
get issuerUrl() {
|
|
245
|
+
return this.issuer;
|
|
246
|
+
}
|
|
247
|
+
getSnapshot() {
|
|
248
|
+
return this.snapshot;
|
|
249
|
+
}
|
|
250
|
+
subscribe(listener) {
|
|
251
|
+
this.listeners.add(listener);
|
|
252
|
+
return () => {
|
|
253
|
+
this.listeners.delete(listener);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* One-time bootstrap: warm the session from the refresh cookie if present.
|
|
258
|
+
* Safe to call multiple times.
|
|
259
|
+
*/
|
|
260
|
+
async bootstrap() {
|
|
261
|
+
if (this.bootstrapped) return;
|
|
262
|
+
this.bootstrapped = true;
|
|
263
|
+
const stored = await Promise.resolve(this.tokenStore.read());
|
|
264
|
+
if (!stored) {
|
|
265
|
+
this.setStatus("unauthenticated");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const ok = await this.refresh();
|
|
269
|
+
if (!ok) this.setStatus("unauthenticated");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Single-flight token refresh, coordinated across tabs via BroadcastChannel.
|
|
273
|
+
*
|
|
274
|
+
* Per-tab: concurrent callers share the same `refreshPromise`.
|
|
275
|
+
* Cross-tab: a tab that wants to refresh first broadcasts a `refresh:claim`
|
|
276
|
+
* with its tabId + timestamp. If it has already received a competing claim
|
|
277
|
+
* with an earlier timestamp (or lower tabId on tie), it waits for that
|
|
278
|
+
* tab's `refresh:done` message instead of issuing its own HTTP request.
|
|
279
|
+
* This prevents two tabs racing on rotating refresh tokens, which would
|
|
280
|
+
* invalidate the session.
|
|
281
|
+
*/
|
|
282
|
+
refresh() {
|
|
283
|
+
if (this.refreshPromise) return this.refreshPromise;
|
|
284
|
+
this.refreshPromise = this.runRefresh().finally(() => {
|
|
285
|
+
this.refreshPromise = null;
|
|
286
|
+
});
|
|
287
|
+
return this.refreshPromise;
|
|
288
|
+
}
|
|
289
|
+
async runRefresh() {
|
|
290
|
+
const myClaim = { source: this.tabId, ts: Date.now() };
|
|
291
|
+
if (this.channel) {
|
|
292
|
+
this.broadcastEnvelope({ type: "refresh:claim", source: myClaim.source, ts: myClaim.ts });
|
|
293
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
294
|
+
const foreign = this.foreignClaim;
|
|
295
|
+
if (foreign && this.claimWins(foreign, myClaim)) {
|
|
296
|
+
return this.waitForForeignRefresh();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const refreshToken = await Promise.resolve(this.tokenStore.read());
|
|
301
|
+
const res = await this.fetchImpl(`${this.issuer}${this.refreshPath}`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
credentials: "include",
|
|
304
|
+
headers: { "Content-Type": "application/json" },
|
|
305
|
+
body: JSON.stringify(refreshToken ? { refreshToken } : {})
|
|
306
|
+
});
|
|
307
|
+
const body = await res.json().catch(() => ({}));
|
|
308
|
+
const data = body.data;
|
|
309
|
+
if (!res.ok || !body.success || !data?.accessToken) {
|
|
310
|
+
const err = body.error;
|
|
311
|
+
this.setError({
|
|
312
|
+
code: err?.code ?? "REFRESH_FAILED",
|
|
313
|
+
message: err?.message ?? `Refresh failed with status ${res.status}`
|
|
314
|
+
});
|
|
315
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (data.refreshToken) {
|
|
319
|
+
await Promise.resolve(this.tokenStore.write(data.refreshToken));
|
|
320
|
+
}
|
|
321
|
+
this.applyAccessToken(data.accessToken);
|
|
322
|
+
this.broadcast("session:refresh");
|
|
323
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: true });
|
|
324
|
+
return true;
|
|
325
|
+
} catch (err) {
|
|
326
|
+
this.setError({
|
|
327
|
+
code: "NETWORK_ERROR",
|
|
328
|
+
message: err instanceof Error ? err.message : "Refresh request failed"
|
|
329
|
+
});
|
|
330
|
+
this.broadcastEnvelope({ type: "refresh:done", source: this.tabId, ts: Date.now(), ok: false });
|
|
331
|
+
return false;
|
|
332
|
+
} finally {
|
|
333
|
+
this.foreignClaim = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
claimWins(foreign, mine) {
|
|
337
|
+
if (foreign.ts < mine.ts) return true;
|
|
338
|
+
if (foreign.ts > mine.ts) return false;
|
|
339
|
+
return foreign.source < mine.source;
|
|
340
|
+
}
|
|
341
|
+
waitForForeignRefresh() {
|
|
342
|
+
return new Promise((resolve) => {
|
|
343
|
+
let settled = false;
|
|
344
|
+
const finish = (ok) => {
|
|
345
|
+
if (settled) return;
|
|
346
|
+
settled = true;
|
|
347
|
+
const idx = this.remoteRefreshWaiters.indexOf(finish);
|
|
348
|
+
if (idx >= 0) this.remoteRefreshWaiters.splice(idx, 1);
|
|
349
|
+
resolve(ok);
|
|
350
|
+
};
|
|
351
|
+
this.remoteRefreshWaiters.push(finish);
|
|
352
|
+
setTimeout(() => finish(this.snapshot.status === "authenticated"), this.crossTabLockTimeoutMs);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Apply an access token (e.g. fresh from a callback exchange) to the
|
|
357
|
+
* session and notify subscribers and other tabs.
|
|
358
|
+
*/
|
|
359
|
+
applyAccessToken(accessToken, refreshToken) {
|
|
360
|
+
const claims = decodeClaims(accessToken);
|
|
361
|
+
const user = claimsToSessionUser(claims);
|
|
362
|
+
if (refreshToken) {
|
|
363
|
+
void Promise.resolve(this.tokenStore.write(refreshToken));
|
|
364
|
+
}
|
|
365
|
+
this.update({
|
|
366
|
+
status: user ? "authenticated" : "unauthenticated",
|
|
367
|
+
accessToken,
|
|
368
|
+
user,
|
|
369
|
+
claims,
|
|
370
|
+
tenantId: claims?.tenantId ?? this.key.tenantId,
|
|
371
|
+
error: null,
|
|
372
|
+
version: this.snapshot.version + 1
|
|
373
|
+
});
|
|
374
|
+
this.scheduleProactiveRefresh();
|
|
375
|
+
this.broadcast("session:update");
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Returns a valid access token, refreshing once if it is expired or about
|
|
379
|
+
* to expire. Resolves to `null` if the session can no longer be revived.
|
|
380
|
+
*/
|
|
381
|
+
async getToken() {
|
|
382
|
+
if (this.snapshot.accessToken && !this.isTokenExpiringSoon(this.snapshot.accessToken)) {
|
|
383
|
+
return this.snapshot.accessToken;
|
|
384
|
+
}
|
|
385
|
+
const ok = await this.refresh();
|
|
386
|
+
return ok ? this.snapshot.accessToken : null;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Browser-safe HTTP wrapper. Attaches the current access token and retries
|
|
390
|
+
* once on 401 (refreshing in between). Rejects with an `IQAuthError` on
|
|
391
|
+
* the second 401 — the caller is then responsible for redirecting to
|
|
392
|
+
* sign-in (typically by letting the unauthenticated state surface through
|
|
393
|
+
* `<SignedOut/>`).
|
|
394
|
+
*/
|
|
395
|
+
async fetch(input, init = {}) {
|
|
396
|
+
const exec = async (token2) => {
|
|
397
|
+
const headers = new Headers(init.headers || {});
|
|
398
|
+
if (token2) headers.set("Authorization", `Bearer ${token2}`);
|
|
399
|
+
return this.fetchImpl(input, {
|
|
400
|
+
...init,
|
|
401
|
+
headers,
|
|
402
|
+
credentials: init.credentials ?? "include"
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
let token = await this.getToken();
|
|
406
|
+
let res = await exec(token);
|
|
407
|
+
if (res.status !== 401) return res;
|
|
408
|
+
const refreshed = await this.refresh();
|
|
409
|
+
if (!refreshed) {
|
|
410
|
+
this.signOutLocal("unauthenticated");
|
|
411
|
+
throw new IQAuthError(
|
|
412
|
+
"TOKEN_EXPIRED",
|
|
413
|
+
"Session refresh failed; user must sign in again",
|
|
414
|
+
401
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
token = this.snapshot.accessToken;
|
|
418
|
+
res = await exec(token);
|
|
419
|
+
if (res.status === 401) {
|
|
420
|
+
this.signOutLocal("unauthenticated");
|
|
421
|
+
throw new IQAuthError(
|
|
422
|
+
"TOKEN_EXPIRED",
|
|
423
|
+
"Authenticated request failed twice with 401; aborting",
|
|
424
|
+
401
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return res;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Clear the session locally and broadcast a sign-out to other tabs. Does
|
|
431
|
+
* not call the server — callers (e.g. signOut helper) are responsible for
|
|
432
|
+
* the server-side logout request.
|
|
433
|
+
*/
|
|
434
|
+
signOutLocal(status = "unauthenticated") {
|
|
435
|
+
void Promise.resolve(this.tokenStore.clear());
|
|
436
|
+
if (this.proactiveTimer) {
|
|
437
|
+
clearTimeout(this.proactiveTimer);
|
|
438
|
+
this.proactiveTimer = null;
|
|
439
|
+
}
|
|
440
|
+
this.update({
|
|
441
|
+
status,
|
|
442
|
+
accessToken: null,
|
|
443
|
+
user: null,
|
|
444
|
+
claims: null,
|
|
445
|
+
tenantId: null,
|
|
446
|
+
error: null,
|
|
447
|
+
version: this.snapshot.version + 1
|
|
448
|
+
});
|
|
449
|
+
this.broadcast("session:signout");
|
|
450
|
+
}
|
|
451
|
+
destroy() {
|
|
452
|
+
if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
|
|
453
|
+
this.proactiveTimer = null;
|
|
454
|
+
if (this.channel) {
|
|
455
|
+
try {
|
|
456
|
+
this.channel.close();
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
this.channel = null;
|
|
461
|
+
this.listeners.clear();
|
|
462
|
+
}
|
|
463
|
+
// --- internals -----------------------------------------------------------
|
|
464
|
+
setStatus(status) {
|
|
465
|
+
if (this.snapshot.status === status) return;
|
|
466
|
+
this.update({ ...this.snapshot, status, version: this.snapshot.version + 1 });
|
|
467
|
+
}
|
|
468
|
+
setError(error) {
|
|
469
|
+
this.update({ ...this.snapshot, error, version: this.snapshot.version + 1 });
|
|
470
|
+
}
|
|
471
|
+
update(next) {
|
|
472
|
+
this.snapshot = next;
|
|
473
|
+
for (const l of this.listeners) l(next);
|
|
474
|
+
}
|
|
475
|
+
isTokenExpiringSoon(token) {
|
|
476
|
+
const claims = decodeClaims(token);
|
|
477
|
+
if (!claims?.exp) return false;
|
|
478
|
+
return claims.exp - Math.floor(Date.now() / 1e3) < 60;
|
|
479
|
+
}
|
|
480
|
+
scheduleProactiveRefresh() {
|
|
481
|
+
if (!this.proactiveRefresh) return;
|
|
482
|
+
if (this.proactiveTimer) {
|
|
483
|
+
clearTimeout(this.proactiveTimer);
|
|
484
|
+
this.proactiveTimer = null;
|
|
485
|
+
}
|
|
486
|
+
const claims = this.snapshot.claims;
|
|
487
|
+
if (!claims?.exp) return;
|
|
488
|
+
const msUntilRefresh = Math.max(5e3, claims.exp * 1e3 - Date.now() - 6e4);
|
|
489
|
+
this.proactiveTimer = setTimeout(() => {
|
|
490
|
+
void this.refresh();
|
|
491
|
+
}, msUntilRefresh);
|
|
492
|
+
}
|
|
493
|
+
broadcast(type) {
|
|
494
|
+
this.broadcastEnvelope({ type, source: this.tabId, ts: Date.now(), payload: this.snapshot });
|
|
495
|
+
}
|
|
496
|
+
broadcastEnvelope(env) {
|
|
497
|
+
if (!this.channel) return;
|
|
498
|
+
try {
|
|
499
|
+
this.channel.postMessage(env);
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
onBroadcast(env) {
|
|
504
|
+
if (!env || env.source === this.tabId) return;
|
|
505
|
+
if (env.type === "refresh:claim") {
|
|
506
|
+
this.foreignClaim = { source: env.source, ts: env.ts };
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (env.type === "refresh:done") {
|
|
510
|
+
const ok = env.ok ?? false;
|
|
511
|
+
const waiters = this.remoteRefreshWaiters;
|
|
512
|
+
this.remoteRefreshWaiters = [];
|
|
513
|
+
for (const w of waiters) w(ok);
|
|
514
|
+
this.foreignClaim = null;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (env.type === "session:signout") {
|
|
518
|
+
this.update({
|
|
519
|
+
status: "unauthenticated",
|
|
520
|
+
accessToken: null,
|
|
521
|
+
user: null,
|
|
522
|
+
claims: null,
|
|
523
|
+
tenantId: null,
|
|
524
|
+
error: null,
|
|
525
|
+
version: this.snapshot.version + 1
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if ((env.type === "session:update" || env.type === "session:refresh") && env.payload) {
|
|
530
|
+
this.update({
|
|
531
|
+
...env.payload,
|
|
532
|
+
version: Math.max(this.snapshot.version, env.payload.version) + 1
|
|
533
|
+
});
|
|
534
|
+
if (this.remoteRefreshWaiters.length) {
|
|
535
|
+
const waiters = this.remoteRefreshWaiters;
|
|
536
|
+
this.remoteRefreshWaiters = [];
|
|
537
|
+
for (const w of waiters) w(true);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/browser/pkce.ts
|
|
544
|
+
function getCrypto() {
|
|
545
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
546
|
+
return globalThis.crypto;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const nodeCrypto = require("crypto").webcrypto;
|
|
550
|
+
if (nodeCrypto) return nodeCrypto;
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
throw new Error("WebCrypto is not available in this environment");
|
|
554
|
+
}
|
|
555
|
+
function base64UrlEncode(bytes) {
|
|
556
|
+
let bin = "";
|
|
557
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
558
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
|
|
559
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
560
|
+
}
|
|
561
|
+
function randomUrlSafe(byteLength = 32) {
|
|
562
|
+
const bytes = new Uint8Array(byteLength);
|
|
563
|
+
getCrypto().getRandomValues(bytes);
|
|
564
|
+
return base64UrlEncode(bytes);
|
|
565
|
+
}
|
|
566
|
+
async function s256Challenge(verifier) {
|
|
567
|
+
const data = new TextEncoder().encode(verifier);
|
|
568
|
+
const digest = await getCrypto().subtle.digest("SHA-256", data);
|
|
569
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
570
|
+
}
|
|
571
|
+
async function createPkcePair() {
|
|
572
|
+
const codeVerifier = randomUrlSafe(32);
|
|
573
|
+
const codeChallenge = await s256Challenge(codeVerifier);
|
|
574
|
+
const state = randomUrlSafe(16);
|
|
575
|
+
const nonce = randomUrlSafe(16);
|
|
576
|
+
return { codeVerifier, codeChallenge, state, nonce };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/browser/signIn.ts
|
|
580
|
+
var DEFAULT_SIGN_IN_PATH = "/sign-in";
|
|
581
|
+
var DEFAULT_LOGOUT_PATH = "/api/v1/auth/logout";
|
|
582
|
+
var DEFAULT_TOKEN_PATH = "/oidc/token";
|
|
583
|
+
var DEFAULT_CALLBACK_PATH = "/auth/callback";
|
|
584
|
+
function defaultRedirectUri() {
|
|
585
|
+
if (typeof window === "undefined") {
|
|
586
|
+
throw new Error("redirectToSignIn requires a browser environment (window)");
|
|
587
|
+
}
|
|
588
|
+
return `${window.location.origin}${DEFAULT_CALLBACK_PATH}`;
|
|
589
|
+
}
|
|
590
|
+
function defaultReturnTo() {
|
|
591
|
+
if (typeof window === "undefined") return "/";
|
|
592
|
+
return window.location.href;
|
|
593
|
+
}
|
|
594
|
+
async function buildSignInUrl(manager, opts = {}) {
|
|
595
|
+
const pkce = await createPkcePair();
|
|
596
|
+
const redirectUri = opts.redirectUri ?? defaultRedirectUri();
|
|
597
|
+
const returnTo = opts.returnTo ?? defaultReturnTo();
|
|
598
|
+
savePkce({
|
|
599
|
+
codeVerifier: pkce.codeVerifier,
|
|
600
|
+
state: pkce.state,
|
|
601
|
+
nonce: pkce.nonce,
|
|
602
|
+
redirectUri,
|
|
603
|
+
appKey: manager.publishableKey.raw,
|
|
604
|
+
returnTo,
|
|
605
|
+
createdAt: Date.now()
|
|
606
|
+
});
|
|
607
|
+
const url = new URL(opts.signInPath ?? DEFAULT_SIGN_IN_PATH, manager.issuerUrl);
|
|
608
|
+
url.searchParams.set("response_type", "code");
|
|
609
|
+
url.searchParams.set("app", manager.appKey);
|
|
610
|
+
url.searchParams.set("publishable_key", manager.publishableKey.raw);
|
|
611
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
612
|
+
url.searchParams.set("state", pkce.state);
|
|
613
|
+
url.searchParams.set("nonce", pkce.nonce);
|
|
614
|
+
url.searchParams.set("code_challenge", pkce.codeChallenge);
|
|
615
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
616
|
+
url.searchParams.set("scope", opts.scope ?? "openid profile email");
|
|
617
|
+
url.searchParams.set("return_to", returnTo);
|
|
618
|
+
return url.toString();
|
|
619
|
+
}
|
|
620
|
+
async function redirectToSignIn(manager, opts = {}) {
|
|
621
|
+
const url = await buildSignInUrl(manager, opts);
|
|
622
|
+
if (typeof window === "undefined") {
|
|
623
|
+
throw new Error("redirectToSignIn requires a browser environment");
|
|
624
|
+
}
|
|
625
|
+
window.location.assign(url);
|
|
626
|
+
}
|
|
627
|
+
async function signIn(manager, opts = {}) {
|
|
628
|
+
return redirectToSignIn(manager, opts);
|
|
629
|
+
}
|
|
630
|
+
async function handleAuthCallback(manager, options = {}) {
|
|
631
|
+
const url = new URL(options.url ?? (typeof window !== "undefined" ? window.location.href : ""));
|
|
632
|
+
const code = url.searchParams.get("code");
|
|
633
|
+
const state = url.searchParams.get("state");
|
|
634
|
+
const errorParam = url.searchParams.get("error");
|
|
635
|
+
if (errorParam) {
|
|
636
|
+
return { ok: false, returnTo: "/", error: errorParam };
|
|
637
|
+
}
|
|
638
|
+
if (!code || !state) {
|
|
639
|
+
return { ok: false, returnTo: "/", error: "missing_code_or_state" };
|
|
640
|
+
}
|
|
641
|
+
const record = loadPkce(state);
|
|
642
|
+
if (!record) {
|
|
643
|
+
return { ok: false, returnTo: "/", error: "unknown_state" };
|
|
644
|
+
}
|
|
645
|
+
clearPkce(state);
|
|
646
|
+
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
|
647
|
+
if (!fetchImpl) {
|
|
648
|
+
return { ok: false, returnTo: record.returnTo, error: "no_fetch" };
|
|
649
|
+
}
|
|
650
|
+
const tokenUrl = `${manager.issuerUrl}${options.tokenPath ?? DEFAULT_TOKEN_PATH}`;
|
|
651
|
+
const res = await fetchImpl(tokenUrl, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
credentials: "include",
|
|
654
|
+
headers: { "Content-Type": "application/json" },
|
|
655
|
+
body: JSON.stringify({
|
|
656
|
+
grant_type: "authorization_code",
|
|
657
|
+
code,
|
|
658
|
+
redirect_uri: record.redirectUri,
|
|
659
|
+
client_id: manager.appKey,
|
|
660
|
+
code_verifier: record.codeVerifier
|
|
661
|
+
})
|
|
662
|
+
});
|
|
663
|
+
const body = await res.json().catch(() => ({}));
|
|
664
|
+
if (!res.ok) {
|
|
665
|
+
const desc = body.error_description ?? body.error ?? "token_exchange_failed";
|
|
666
|
+
return { ok: false, returnTo: record.returnTo, error: desc };
|
|
667
|
+
}
|
|
668
|
+
const tokens = body;
|
|
669
|
+
if (!tokens.access_token) {
|
|
670
|
+
return { ok: false, returnTo: record.returnTo, error: "missing_access_token" };
|
|
671
|
+
}
|
|
672
|
+
if (tokens.refresh_token) {
|
|
673
|
+
setCookie(REFRESH_COOKIE, tokens.refresh_token, { maxAgeSeconds: 60 * 60 * 24 * 30 });
|
|
674
|
+
}
|
|
675
|
+
manager.applyAccessToken(tokens.access_token, tokens.refresh_token);
|
|
676
|
+
return { ok: true, returnTo: record.returnTo };
|
|
677
|
+
}
|
|
678
|
+
async function signOut(manager, opts = {}) {
|
|
679
|
+
if (!opts.localOnly) {
|
|
680
|
+
try {
|
|
681
|
+
const url = `${manager.issuerUrl}${opts.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
|
|
682
|
+
await manager.fetch(url, { method: "POST" }).catch(() => void 0);
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
clearCookie(REFRESH_COOKIE);
|
|
687
|
+
manager.signOutLocal();
|
|
688
|
+
if (opts.returnTo && typeof window !== "undefined") {
|
|
689
|
+
window.location.assign(opts.returnTo);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/react/index.tsx
|
|
694
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
695
|
+
var IQAuthContext = (0, import_react.createContext)(null);
|
|
696
|
+
function IQAuthProvider({
|
|
697
|
+
publishableKey,
|
|
698
|
+
issuer,
|
|
699
|
+
channelName,
|
|
700
|
+
proactiveRefresh,
|
|
701
|
+
manager: externalManager,
|
|
702
|
+
children
|
|
703
|
+
}) {
|
|
704
|
+
const managerRef = (0, import_react.useRef)(null);
|
|
705
|
+
if (!managerRef.current) {
|
|
706
|
+
managerRef.current = externalManager ?? new SessionManager({
|
|
707
|
+
publishableKey,
|
|
708
|
+
issuer,
|
|
709
|
+
channelName,
|
|
710
|
+
proactiveRefresh
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
const manager = managerRef.current;
|
|
714
|
+
const subscribe = (0, import_react.useCallback)(
|
|
715
|
+
(cb) => manager.subscribe(() => cb()),
|
|
716
|
+
[manager]
|
|
717
|
+
);
|
|
718
|
+
const getSnapshot = (0, import_react.useCallback)(() => manager.getSnapshot(), [manager]);
|
|
719
|
+
const getServerSnapshot = (0, import_react.useCallback)(() => manager.getSnapshot(), [manager]);
|
|
720
|
+
const snapshot = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot);
|
|
721
|
+
(0, import_react.useEffect)(() => {
|
|
722
|
+
void manager.bootstrap();
|
|
723
|
+
const onVisible = () => {
|
|
724
|
+
if (typeof document !== "undefined" && document.visibilityState === "visible") {
|
|
725
|
+
void manager.refresh();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
if (typeof document !== "undefined") {
|
|
729
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
730
|
+
}
|
|
731
|
+
return () => {
|
|
732
|
+
if (typeof document !== "undefined") {
|
|
733
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}, [manager]);
|
|
737
|
+
const value = (0, import_react.useMemo)(() => ({ manager, snapshot }), [manager, snapshot]);
|
|
738
|
+
return (0, import_react.createElement)(IQAuthContext.Provider, { value }, children);
|
|
739
|
+
}
|
|
740
|
+
function useCtx() {
|
|
741
|
+
const ctx = (0, import_react.useContext)(IQAuthContext);
|
|
742
|
+
if (!ctx) throw new Error("IQAuth hooks must be used inside <IQAuthProvider>");
|
|
743
|
+
return ctx;
|
|
744
|
+
}
|
|
745
|
+
function useUser() {
|
|
746
|
+
const { snapshot } = useCtx();
|
|
747
|
+
return (0, import_react.useMemo)(
|
|
748
|
+
() => ({
|
|
749
|
+
isLoaded: snapshot.status !== "loading",
|
|
750
|
+
isSignedIn: snapshot.status === "authenticated" && !!snapshot.user,
|
|
751
|
+
user: snapshot.user,
|
|
752
|
+
error: snapshot.error
|
|
753
|
+
}),
|
|
754
|
+
[snapshot.status, snapshot.user, snapshot.error, snapshot.version]
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
function useSession() {
|
|
758
|
+
const { snapshot } = useCtx();
|
|
759
|
+
return (0, import_react.useMemo)(
|
|
760
|
+
() => ({
|
|
761
|
+
isLoaded: snapshot.status !== "loading",
|
|
762
|
+
isSignedIn: snapshot.status === "authenticated",
|
|
763
|
+
claims: snapshot.claims,
|
|
764
|
+
accessToken: snapshot.accessToken,
|
|
765
|
+
error: snapshot.error
|
|
766
|
+
}),
|
|
767
|
+
[snapshot.status, snapshot.claims, snapshot.accessToken, snapshot.error, snapshot.version]
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
function useAuth() {
|
|
771
|
+
const { manager, snapshot } = useCtx();
|
|
772
|
+
return (0, import_react.useMemo)(
|
|
773
|
+
() => ({
|
|
774
|
+
isLoaded: snapshot.status !== "loading",
|
|
775
|
+
isSignedIn: snapshot.status === "authenticated",
|
|
776
|
+
userId: snapshot.user?.sub ?? null,
|
|
777
|
+
tenantId: snapshot.tenantId,
|
|
778
|
+
error: snapshot.error,
|
|
779
|
+
signIn: (opts) => signIn(manager, opts),
|
|
780
|
+
signOut: (opts) => signOut(manager, opts),
|
|
781
|
+
redirectToSignIn: (opts) => redirectToSignIn(manager, opts),
|
|
782
|
+
getToken: () => manager.getToken(),
|
|
783
|
+
fetch: (input, init) => manager.fetch(input, init)
|
|
784
|
+
}),
|
|
785
|
+
[manager, snapshot.status, snapshot.user, snapshot.tenantId, snapshot.error, snapshot.version]
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
function useOrganization() {
|
|
789
|
+
const { snapshot } = useCtx();
|
|
790
|
+
return (0, import_react.useMemo)(
|
|
791
|
+
() => ({
|
|
792
|
+
isLoaded: snapshot.status !== "loading",
|
|
793
|
+
organization: snapshot.tenantId ? { id: snapshot.tenantId, tenantId: snapshot.tenantId } : null,
|
|
794
|
+
error: snapshot.error
|
|
795
|
+
}),
|
|
796
|
+
[snapshot.status, snapshot.tenantId, snapshot.error, snapshot.version]
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
function useAuthFetch() {
|
|
800
|
+
const { manager } = useCtx();
|
|
801
|
+
return (0, import_react.useCallback)(
|
|
802
|
+
(input, init) => manager.fetch(input, init),
|
|
803
|
+
[manager]
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
function SignedIn({ children }) {
|
|
807
|
+
const { isSignedIn, isLoaded } = useUser();
|
|
808
|
+
if (!isLoaded || !isSignedIn) return null;
|
|
809
|
+
return (0, import_react.createElement)(import_react.Fragment, null, children);
|
|
810
|
+
}
|
|
811
|
+
function SignedOut({ children }) {
|
|
812
|
+
const { isSignedIn, isLoaded } = useUser();
|
|
813
|
+
if (!isLoaded || isSignedIn) return null;
|
|
814
|
+
return (0, import_react.createElement)(import_react.Fragment, null, children);
|
|
815
|
+
}
|
|
816
|
+
function RedirectToSignIn(props = {}) {
|
|
817
|
+
const { manager, snapshot } = useCtx();
|
|
818
|
+
(0, import_react.useEffect)(() => {
|
|
819
|
+
if (snapshot.status === "unauthenticated") {
|
|
820
|
+
void redirectToSignIn(manager, props);
|
|
821
|
+
}
|
|
822
|
+
}, [manager, snapshot.status]);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
function AuthCallback({ onComplete, fallback } = {}) {
|
|
826
|
+
const { manager } = useCtx();
|
|
827
|
+
(0, import_react.useEffect)(() => {
|
|
828
|
+
let cancelled = false;
|
|
829
|
+
void handleAuthCallback(manager).then((result) => {
|
|
830
|
+
if (cancelled) return;
|
|
831
|
+
if (onComplete) onComplete(result);
|
|
832
|
+
else if (typeof window !== "undefined") {
|
|
833
|
+
window.location.replace(result.returnTo || "/");
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
return () => {
|
|
837
|
+
cancelled = true;
|
|
838
|
+
};
|
|
839
|
+
}, [manager, onComplete]);
|
|
840
|
+
return (0, import_react.createElement)(import_react.Fragment, null, fallback ?? null);
|
|
841
|
+
}
|
|
842
|
+
function brandStyle(branding) {
|
|
843
|
+
if (!branding) return {};
|
|
844
|
+
const s = {};
|
|
845
|
+
if (branding.primaryColor) s["--brand-primary"] = branding.primaryColor;
|
|
846
|
+
if (branding.accentColor) s["--brand-accent"] = branding.accentColor;
|
|
847
|
+
if (branding.backgroundColor) s["--brand-bg"] = branding.backgroundColor;
|
|
848
|
+
if (branding.surfaceColor) s["--brand-surface"] = branding.surfaceColor;
|
|
849
|
+
if (branding.textColor) s["--brand-text"] = branding.textColor;
|
|
850
|
+
return s;
|
|
851
|
+
}
|
|
852
|
+
async function jsonFetch(url, init) {
|
|
853
|
+
const res = await fetch(url, { ...init, credentials: init?.credentials || "include" });
|
|
854
|
+
const payload = await res.json().catch(() => ({}));
|
|
855
|
+
if (!res.ok && payload?.success !== true) {
|
|
856
|
+
const message = payload?.error?.message || payload?.error_description || payload?.error || `HTTP ${res.status}`;
|
|
857
|
+
throw new Error(typeof message === "string" ? message : "Request failed");
|
|
858
|
+
}
|
|
859
|
+
return payload;
|
|
860
|
+
}
|
|
861
|
+
function useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo) {
|
|
862
|
+
const [ctx, setCtx] = (0, import_react.useState)(null);
|
|
863
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
864
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
865
|
+
(0, import_react.useEffect)(() => {
|
|
866
|
+
if (!appKey) {
|
|
867
|
+
setLoading(false);
|
|
868
|
+
setError("appKey is required");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
let cancelled = false;
|
|
872
|
+
setLoading(true);
|
|
873
|
+
const url = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/public/apps/${encodeURIComponent(appKey)}/sign-in-context?return_to=${encodeURIComponent(returnTo)}`;
|
|
874
|
+
fetch(url).then((r) => r.json()).then((payload) => {
|
|
875
|
+
if (cancelled) return;
|
|
876
|
+
if (payload?.success === false) throw new Error(payload?.error?.message || "Failed to load sign-in context");
|
|
877
|
+
setCtx(payload.data);
|
|
878
|
+
}).catch((err) => {
|
|
879
|
+
if (!cancelled) setError(err.message);
|
|
880
|
+
}).finally(() => {
|
|
881
|
+
if (!cancelled) setLoading(false);
|
|
882
|
+
});
|
|
883
|
+
return () => {
|
|
884
|
+
cancelled = true;
|
|
885
|
+
};
|
|
886
|
+
}, [iqAuthBaseUrl, appKey, returnTo]);
|
|
887
|
+
return { ctx, loading, error };
|
|
888
|
+
}
|
|
889
|
+
function Shell({
|
|
890
|
+
branding,
|
|
891
|
+
className,
|
|
892
|
+
children
|
|
893
|
+
}) {
|
|
894
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
895
|
+
"div",
|
|
896
|
+
{
|
|
897
|
+
className,
|
|
898
|
+
style: {
|
|
899
|
+
background: "var(--brand-surface, #ffffff)",
|
|
900
|
+
color: "var(--brand-text, #0f172a)",
|
|
901
|
+
border: "1px solid rgba(15,23,42,0.08)",
|
|
902
|
+
borderRadius: 12,
|
|
903
|
+
padding: 24,
|
|
904
|
+
...brandStyle(branding)
|
|
905
|
+
},
|
|
906
|
+
children: [
|
|
907
|
+
branding?.logoUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: branding.logoUrl, alt: branding.brandName || "", style: { height: 28, width: "auto", marginBottom: 16 } }) : null,
|
|
908
|
+
children
|
|
909
|
+
]
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
function Field({ label, children }) {
|
|
914
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: 13 }, children: [
|
|
915
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: 500 }, children: label }),
|
|
916
|
+
children
|
|
917
|
+
] });
|
|
918
|
+
}
|
|
919
|
+
function inputStyle() {
|
|
920
|
+
return {
|
|
921
|
+
padding: "8px 12px",
|
|
922
|
+
border: "1px solid rgba(15,23,42,0.15)",
|
|
923
|
+
borderRadius: 6,
|
|
924
|
+
fontSize: 14,
|
|
925
|
+
width: "100%",
|
|
926
|
+
background: "transparent",
|
|
927
|
+
color: "inherit"
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function PrimaryButton(props) {
|
|
931
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
932
|
+
"button",
|
|
933
|
+
{
|
|
934
|
+
...props,
|
|
935
|
+
style: {
|
|
936
|
+
background: "var(--brand-primary, #3b82f6)",
|
|
937
|
+
color: "#fff",
|
|
938
|
+
border: "none",
|
|
939
|
+
padding: "10px 16px",
|
|
940
|
+
borderRadius: 8,
|
|
941
|
+
fontSize: 14,
|
|
942
|
+
fontWeight: 500,
|
|
943
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
944
|
+
opacity: props.disabled ? 0.6 : 1,
|
|
945
|
+
width: "100%",
|
|
946
|
+
...props.style
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
function GhostButton(props) {
|
|
952
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
953
|
+
"button",
|
|
954
|
+
{
|
|
955
|
+
...props,
|
|
956
|
+
style: {
|
|
957
|
+
background: "transparent",
|
|
958
|
+
color: "inherit",
|
|
959
|
+
border: "1px solid rgba(15,23,42,0.15)",
|
|
960
|
+
padding: "10px 16px",
|
|
961
|
+
borderRadius: 8,
|
|
962
|
+
fontSize: 14,
|
|
963
|
+
fontWeight: 500,
|
|
964
|
+
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
965
|
+
width: "100%",
|
|
966
|
+
...props.style
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
function ErrorBanner({ message }) {
|
|
972
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: {
|
|
973
|
+
borderLeft: "3px solid #dc2626",
|
|
974
|
+
background: "rgba(220,38,38,0.08)",
|
|
975
|
+
padding: "10px 14px",
|
|
976
|
+
borderRadius: "0 6px 6px 0",
|
|
977
|
+
fontSize: 13,
|
|
978
|
+
color: "#b91c1c"
|
|
979
|
+
}, children: message });
|
|
980
|
+
}
|
|
981
|
+
function SignIn({ iqAuthBaseUrl, appKey, returnTo, onRedirect, className }) {
|
|
982
|
+
const { ctx, loading, error } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo);
|
|
983
|
+
const [email, setEmail] = (0, import_react.useState)("");
|
|
984
|
+
const [password, setPassword] = (0, import_react.useState)("");
|
|
985
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
986
|
+
const [formError, setFormError] = (0, import_react.useState)("");
|
|
987
|
+
const [mfa, setMfa] = (0, import_react.useState)(null);
|
|
988
|
+
const [tenantSel, setTenantSel] = (0, import_react.useState)(null);
|
|
989
|
+
const [oauthExchanging, setOauthExchanging] = (0, import_react.useState)(false);
|
|
990
|
+
const oidcPayload = () => ({
|
|
991
|
+
client_id: ctx?.app.defaultClientId,
|
|
992
|
+
redirect_uri: returnTo,
|
|
993
|
+
scope: "openid"
|
|
994
|
+
});
|
|
995
|
+
const handlePayload = (payload) => {
|
|
996
|
+
if (payload.type === "redirect" && payload.redirectUrl) {
|
|
997
|
+
(onRedirect || ((u) => {
|
|
998
|
+
window.location.href = u;
|
|
999
|
+
}))(payload.redirectUrl);
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
if (payload.type === "tenant_selection") {
|
|
1003
|
+
setTenantSel({ token: payload.tenantSelectionToken, tenants: payload.tenants || [] });
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
if (payload.type === "mfa_required") {
|
|
1007
|
+
const methods = payload.availableMethods || ["totp"];
|
|
1008
|
+
setMfa({ token: payload.mfaChallengeToken, methods, selected: methods[0], code: "", backup: false });
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
};
|
|
1013
|
+
const submitLogin = async (e) => {
|
|
1014
|
+
e.preventDefault();
|
|
1015
|
+
if (!ctx?.app.defaultClientId) {
|
|
1016
|
+
setFormError("Application is not configured for hosted sign-in.");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
setSubmitting(true);
|
|
1020
|
+
setFormError("");
|
|
1021
|
+
try {
|
|
1022
|
+
const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-login`, {
|
|
1023
|
+
method: "POST",
|
|
1024
|
+
headers: { "Content-Type": "application/json" },
|
|
1025
|
+
credentials: "include",
|
|
1026
|
+
body: JSON.stringify({ email, password, ...oidcPayload() })
|
|
1027
|
+
});
|
|
1028
|
+
const payload = await r.json().catch(() => ({}));
|
|
1029
|
+
if (!handlePayload(payload)) setFormError(payload.error_description || payload.error || "Sign-in failed");
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
setFormError(err.message || "Network error");
|
|
1032
|
+
}
|
|
1033
|
+
setSubmitting(false);
|
|
1034
|
+
};
|
|
1035
|
+
const submitMfa = async (e) => {
|
|
1036
|
+
e.preventDefault();
|
|
1037
|
+
if (!mfa) return;
|
|
1038
|
+
setSubmitting(true);
|
|
1039
|
+
setFormError("");
|
|
1040
|
+
const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-mfa-complete`, {
|
|
1041
|
+
method: "POST",
|
|
1042
|
+
headers: { "Content-Type": "application/json" },
|
|
1043
|
+
credentials: "include",
|
|
1044
|
+
body: JSON.stringify({
|
|
1045
|
+
mfaChallengeToken: mfa.token,
|
|
1046
|
+
code: mfa.code,
|
|
1047
|
+
method: mfa.selected,
|
|
1048
|
+
useBackup: mfa.backup,
|
|
1049
|
+
...oidcPayload()
|
|
1050
|
+
})
|
|
1051
|
+
});
|
|
1052
|
+
const payload = await r.json().catch(() => ({}));
|
|
1053
|
+
if (!handlePayload(payload)) setFormError(payload.error_description || payload.error || "MFA verification failed");
|
|
1054
|
+
setSubmitting(false);
|
|
1055
|
+
};
|
|
1056
|
+
const submitTenant = async (tenantId) => {
|
|
1057
|
+
if (!tenantSel) return;
|
|
1058
|
+
setSubmitting(true);
|
|
1059
|
+
setFormError("");
|
|
1060
|
+
const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-tenant-select`, {
|
|
1061
|
+
method: "POST",
|
|
1062
|
+
headers: { "Content-Type": "application/json" },
|
|
1063
|
+
credentials: "include",
|
|
1064
|
+
body: JSON.stringify({ tenantSelectionToken: tenantSel.token, tenantId, ...oidcPayload() })
|
|
1065
|
+
});
|
|
1066
|
+
const payload = await r.json().catch(() => ({}));
|
|
1067
|
+
if (!handlePayload(payload)) setFormError(payload.error_description || payload.error || "Tenant selection failed");
|
|
1068
|
+
setSubmitting(false);
|
|
1069
|
+
};
|
|
1070
|
+
const startGoogleLogin = () => {
|
|
1071
|
+
if (!ctx?.app.defaultClientId) {
|
|
1072
|
+
setFormError("Application is not configured for hosted sign-in.");
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const bridgeUrl = window.location.href;
|
|
1076
|
+
const url = `${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/google?redirect_uri=${encodeURIComponent(bridgeUrl)}&client_id=${encodeURIComponent(ctx.app.defaultClientId)}`;
|
|
1077
|
+
window.location.href = url;
|
|
1078
|
+
};
|
|
1079
|
+
(0, import_react.useEffect)(() => {
|
|
1080
|
+
if (!ctx?.app.defaultClientId) return;
|
|
1081
|
+
const params = new URLSearchParams(window.location.search);
|
|
1082
|
+
const oauthCode = params.get("code");
|
|
1083
|
+
if (!oauthCode) return;
|
|
1084
|
+
const u = new URL(window.location.href);
|
|
1085
|
+
u.searchParams.delete("code");
|
|
1086
|
+
const codeRedirectUri = u.toString();
|
|
1087
|
+
setOauthExchanging(true);
|
|
1088
|
+
setFormError("");
|
|
1089
|
+
(async () => {
|
|
1090
|
+
try {
|
|
1091
|
+
const r = await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/oidc/sso-complete-oauth`, {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
headers: { "Content-Type": "application/json" },
|
|
1094
|
+
credentials: "include",
|
|
1095
|
+
body: JSON.stringify({
|
|
1096
|
+
authCode: oauthCode,
|
|
1097
|
+
code_redirect_uri: codeRedirectUri,
|
|
1098
|
+
...oidcPayload()
|
|
1099
|
+
})
|
|
1100
|
+
});
|
|
1101
|
+
const payload = await r.json().catch(() => ({}));
|
|
1102
|
+
try {
|
|
1103
|
+
window.history.replaceState({}, "", u.pathname + (u.search ? u.search : "") + u.hash);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
if (!handlePayload(payload)) setFormError(payload.error_description || payload.error || "Authorization code exchange failed");
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
setFormError(err.message || "Authorization code exchange failed");
|
|
1109
|
+
}
|
|
1110
|
+
setOauthExchanging(false);
|
|
1111
|
+
})();
|
|
1112
|
+
}, [ctx?.app.defaultClientId]);
|
|
1113
|
+
if (loading || oauthExchanging) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: ctx?.branding || null, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: oauthExchanging ? "Completing sign-in\u2026" : "Loading\u2026" }) });
|
|
1114
|
+
if (error || !ctx) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: null, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error || "Failed to load app context" }) });
|
|
1115
|
+
if (!ctx.returnAllowed) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: ctx.branding, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: `returnTo "${returnTo}" is not in this app's allowed origins.` }) });
|
|
1116
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding: ctx.branding, className, children: [
|
|
1117
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: { fontSize: 20, fontWeight: 600, margin: "0 0 12px" }, children: ctx.branding?.loginHeadline || `Sign in to ${ctx.app.name}` }),
|
|
1118
|
+
ctx.branding?.loginSubheadline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { marginBottom: 16, fontSize: 13, opacity: 0.7 }, children: ctx.branding.loginSubheadline }) : null,
|
|
1119
|
+
formError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: 12 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: formError }) }) : null,
|
|
1120
|
+
tenantSel ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-label": "Choose tenant", style: { display: "flex", flexDirection: "column", gap: 8 }, children: tenantSel.tenants.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1121
|
+
"button",
|
|
1122
|
+
{
|
|
1123
|
+
type: "button",
|
|
1124
|
+
"data-iqauth-tenant": t.tenantId,
|
|
1125
|
+
onClick: () => submitTenant(t.tenantId),
|
|
1126
|
+
style: { textAlign: "left", padding: "10px 14px", border: "1px solid rgba(15,23,42,0.15)", borderRadius: 8, background: "transparent", color: "inherit", cursor: "pointer" },
|
|
1127
|
+
children: [
|
|
1128
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: t.tenantName || t.tenantSlug || t.tenantId }),
|
|
1129
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 12, opacity: 0.6 }, children: t.roles.join(", ") })
|
|
1130
|
+
]
|
|
1131
|
+
},
|
|
1132
|
+
t.tenantId
|
|
1133
|
+
)) }) : mfa ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": "MFA verification", children: [
|
|
1134
|
+
!mfa.backup && mfa.methods.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Method", 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,
|
|
1135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: mfa.backup ? "Backup code" : "Verification code", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1136
|
+
"input",
|
|
1137
|
+
{
|
|
1138
|
+
style: { ...inputStyle(), fontFamily: "monospace", textAlign: mfa.backup ? "left" : "center", letterSpacing: mfa.backup ? "0.04em" : "0.3em" },
|
|
1139
|
+
value: mfa.code,
|
|
1140
|
+
onChange: (e) => setMfa({ ...mfa, code: e.target.value }),
|
|
1141
|
+
autoComplete: "one-time-code",
|
|
1142
|
+
inputMode: mfa.backup ? "text" : "numeric"
|
|
1143
|
+
}
|
|
1144
|
+
) }),
|
|
1145
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !mfa.code, children: submitting ? "Verifying\u2026" : "Verify" }),
|
|
1146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(GhostButton, { type: "button", onClick: () => setMfa({ ...mfa, backup: !mfa.backup, code: "" }), children: mfa.backup ? "Use verification code" : "Use backup code" })
|
|
1147
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
1148
|
+
ctx.providers?.google ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1149
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(GhostButton, { type: "button", onClick: startGoogleLogin, disabled: submitting, "aria-label": "Continue with Google", style: { display: "flex", alignItems: "center", justifyContent: "center", gap: 10 }, children: [
|
|
1150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": "true", children: [
|
|
1151
|
+
/* @__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" }),
|
|
1152
|
+
/* @__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" }),
|
|
1153
|
+
/* @__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" }),
|
|
1154
|
+
/* @__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" })
|
|
1155
|
+
] }),
|
|
1156
|
+
"Continue with Google"
|
|
1157
|
+
] }),
|
|
1158
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "separator", "aria-label": "or", style: { display: "flex", alignItems: "center", gap: 10, fontSize: 12, opacity: 0.6 }, children: [
|
|
1159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, height: 1, background: "rgba(15,23,42,0.12)" } }),
|
|
1160
|
+
"OR",
|
|
1161
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, height: 1, background: "rgba(15,23,42,0.12)" } })
|
|
1162
|
+
] })
|
|
1163
|
+
] }) : null,
|
|
1164
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitLogin, style: { display: "flex", flexDirection: "column", gap: 12 }, "aria-label": `Sign in to ${ctx.app.name}`, children: [
|
|
1165
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Email", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "email", autoComplete: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value) }) }),
|
|
1166
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Password", 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) }) }),
|
|
1167
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password, children: submitting ? "Signing in\u2026" : "Sign in" })
|
|
1168
|
+
] })
|
|
1169
|
+
] })
|
|
1170
|
+
] });
|
|
1171
|
+
}
|
|
1172
|
+
function SignUp({ iqAuthBaseUrl, appKey, returnTo, onSuccess, className }) {
|
|
1173
|
+
const { ctx, loading } = useIQAuthSignInContext(iqAuthBaseUrl, appKey, returnTo || "");
|
|
1174
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
1175
|
+
const [email, setEmail] = (0, import_react.useState)("");
|
|
1176
|
+
const [password, setPassword] = (0, import_react.useState)("");
|
|
1177
|
+
const [organizationName, setOrganizationName] = (0, import_react.useState)("");
|
|
1178
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(false);
|
|
1179
|
+
const [error, setError] = (0, import_react.useState)("");
|
|
1180
|
+
const [done, setDone] = (0, import_react.useState)(false);
|
|
1181
|
+
const submit = async (e) => {
|
|
1182
|
+
e.preventDefault();
|
|
1183
|
+
setSubmitting(true);
|
|
1184
|
+
setError("");
|
|
1185
|
+
try {
|
|
1186
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/signup`, {
|
|
1187
|
+
method: "POST",
|
|
1188
|
+
headers: { "Content-Type": "application/json" },
|
|
1189
|
+
body: JSON.stringify({ email, name, password, organizationName: organizationName || void 0 })
|
|
1190
|
+
});
|
|
1191
|
+
setDone(true);
|
|
1192
|
+
onSuccess?.();
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
setError(err.message);
|
|
1195
|
+
}
|
|
1196
|
+
setSubmitting(false);
|
|
1197
|
+
};
|
|
1198
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: null, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Loading\u2026" }) });
|
|
1199
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding: ctx?.branding || null, className, children: [
|
|
1200
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: { fontSize: 20, fontWeight: 600, margin: "0 0 12px" }, children: "Create your account" }),
|
|
1201
|
+
done ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Account created. Check your email for verification." }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
1202
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: error }) : null,
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Full name", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), value: name, onChange: (e) => setName(e.target.value), required: true }) }),
|
|
1204
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Email", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true }) }),
|
|
1205
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Organization (optional)", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { style: inputStyle(), value: organizationName, onChange: (e) => setOrganizationName(e.target.value) }) }),
|
|
1206
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Password", 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 }) }),
|
|
1207
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: submitting || !email || !password || !name, children: submitting ? "Creating\u2026" : "Create account" })
|
|
1208
|
+
] })
|
|
1209
|
+
] });
|
|
1210
|
+
}
|
|
1211
|
+
function initialsOf(name, email) {
|
|
1212
|
+
const src = name || email || "?";
|
|
1213
|
+
const parts = src.split(/[\s@]+/).filter(Boolean);
|
|
1214
|
+
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
1215
|
+
return src.substring(0, 2).toUpperCase();
|
|
1216
|
+
}
|
|
1217
|
+
function UserButton({ iqAuthBaseUrl, accountUrl, onSignOut, className }) {
|
|
1218
|
+
const [user, setUser] = (0, import_react.useState)(null);
|
|
1219
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1220
|
+
(0, import_react.useEffect)(() => {
|
|
1221
|
+
let cancelled = false;
|
|
1222
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
1223
|
+
if (!cancelled && p?.data) setUser(p.data);
|
|
1224
|
+
}).catch(() => {
|
|
1225
|
+
});
|
|
1226
|
+
return () => {
|
|
1227
|
+
cancelled = true;
|
|
1228
|
+
};
|
|
1229
|
+
}, [iqAuthBaseUrl]);
|
|
1230
|
+
const signOut2 = async () => {
|
|
1231
|
+
try {
|
|
1232
|
+
await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/logout`, { method: "POST", credentials: "include" });
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
if (onSignOut) onSignOut();
|
|
1236
|
+
else window.location.reload();
|
|
1237
|
+
};
|
|
1238
|
+
if (!user) return null;
|
|
1239
|
+
const target = accountUrl || `${iqAuthBaseUrl.replace(/\/$/, "")}/account`;
|
|
1240
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: { position: "relative", display: "inline-block" }, children: [
|
|
1241
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1242
|
+
"button",
|
|
1243
|
+
{
|
|
1244
|
+
type: "button",
|
|
1245
|
+
"aria-haspopup": "menu",
|
|
1246
|
+
"aria-expanded": open,
|
|
1247
|
+
onClick: () => setOpen((o) => !o),
|
|
1248
|
+
style: {
|
|
1249
|
+
width: 32,
|
|
1250
|
+
height: 32,
|
|
1251
|
+
borderRadius: "50%",
|
|
1252
|
+
background: "var(--brand-accent, #6366f1)",
|
|
1253
|
+
color: "#fff",
|
|
1254
|
+
border: "none",
|
|
1255
|
+
cursor: "pointer",
|
|
1256
|
+
fontSize: 12,
|
|
1257
|
+
fontWeight: 600
|
|
1258
|
+
},
|
|
1259
|
+
children: user.picture ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: user.picture, alt: user.name, style: { width: "100%", height: "100%", borderRadius: "50%" } }) : initialsOf(user.name, user.email)
|
|
1260
|
+
}
|
|
1261
|
+
),
|
|
1262
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "menu", style: {
|
|
1263
|
+
position: "absolute",
|
|
1264
|
+
right: 0,
|
|
1265
|
+
top: 40,
|
|
1266
|
+
minWidth: 200,
|
|
1267
|
+
background: "#fff",
|
|
1268
|
+
border: "1px solid rgba(15,23,42,0.12)",
|
|
1269
|
+
borderRadius: 8,
|
|
1270
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
1271
|
+
padding: 8,
|
|
1272
|
+
zIndex: 100
|
|
1273
|
+
}, children: [
|
|
1274
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "8px 10px", fontSize: 12, opacity: 0.7, borderBottom: "1px solid rgba(15,23,42,0.06)" }, children: [
|
|
1275
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500, color: "#0f172a" }, children: user.name }),
|
|
1276
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: user.email })
|
|
1277
|
+
] }),
|
|
1278
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: target, role: "menuitem", style: { display: "block", padding: "8px 10px", fontSize: 13, color: "#0f172a", textDecoration: "none" }, children: "Account" }),
|
|
1279
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1280
|
+
"button",
|
|
1281
|
+
{
|
|
1282
|
+
role: "menuitem",
|
|
1283
|
+
type: "button",
|
|
1284
|
+
onClick: signOut2,
|
|
1285
|
+
style: { display: "block", width: "100%", textAlign: "left", padding: "8px 10px", fontSize: 13, background: "transparent", border: "none", cursor: "pointer", color: "#b91c1c" },
|
|
1286
|
+
children: "Sign out"
|
|
1287
|
+
}
|
|
1288
|
+
)
|
|
1289
|
+
] }) : null
|
|
1290
|
+
] });
|
|
1291
|
+
}
|
|
1292
|
+
function UserProfile({ iqAuthBaseUrl, className }) {
|
|
1293
|
+
const [user, setUser] = (0, import_react.useState)(null);
|
|
1294
|
+
const [oldPassword, setOldPassword] = (0, import_react.useState)("");
|
|
1295
|
+
const [newPassword, setNewPassword] = (0, import_react.useState)("");
|
|
1296
|
+
const [pwState, setPwState] = (0, import_react.useState)({ submitting: false, message: "", error: "" });
|
|
1297
|
+
const [sessions, setSessions] = (0, import_react.useState)([]);
|
|
1298
|
+
(0, import_react.useEffect)(() => {
|
|
1299
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
1300
|
+
if (p?.data) setUser(p.data);
|
|
1301
|
+
}).catch(() => {
|
|
1302
|
+
});
|
|
1303
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions`, { credentials: "include" }).then((r) => r.json()).then((p) => setSessions(p?.data?.sessions || p?.data || [])).catch(() => {
|
|
1304
|
+
});
|
|
1305
|
+
}, [iqAuthBaseUrl]);
|
|
1306
|
+
const changePassword = async (e) => {
|
|
1307
|
+
e.preventDefault();
|
|
1308
|
+
setPwState({ submitting: true, message: "", error: "" });
|
|
1309
|
+
try {
|
|
1310
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/password/change`, {
|
|
1311
|
+
method: "POST",
|
|
1312
|
+
headers: { "Content-Type": "application/json" },
|
|
1313
|
+
body: JSON.stringify({ oldPassword, newPassword })
|
|
1314
|
+
});
|
|
1315
|
+
setPwState({ submitting: false, message: "Password updated.", error: "" });
|
|
1316
|
+
setOldPassword("");
|
|
1317
|
+
setNewPassword("");
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
setPwState({ submitting: false, message: "", error: err.message });
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
const revoke = async (sessionId) => {
|
|
1323
|
+
await fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/sessions/${sessionId}`, { method: "DELETE", credentials: "include" });
|
|
1324
|
+
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
|
1325
|
+
};
|
|
1326
|
+
if (!user) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shell, { branding: null, className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Loading account\u2026" }) });
|
|
1327
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Shell, { branding: null, className, children: [
|
|
1328
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: { fontSize: 20, fontWeight: 600, margin: "0 0 12px" }, children: "Your account" }),
|
|
1329
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-profile", style: { marginBottom: 20 }, children: [
|
|
1330
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-profile", style: { fontSize: 14, fontWeight: 600 }, children: "Profile" }),
|
|
1331
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
|
|
1332
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Name:" }),
|
|
1333
|
+
" ",
|
|
1334
|
+
user.name
|
|
1335
|
+
] }),
|
|
1336
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { fontSize: 13, margin: "4px 0" }, children: [
|
|
1337
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Email:" }),
|
|
1338
|
+
" ",
|
|
1339
|
+
user.email
|
|
1340
|
+
] })
|
|
1341
|
+
] }),
|
|
1342
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-pw", style: { marginBottom: 20 }, children: [
|
|
1343
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-pw", style: { fontSize: 14, fontWeight: 600 }, children: "Change password" }),
|
|
1344
|
+
pwState.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, { message: pwState.error }) : null,
|
|
1345
|
+
pwState.message ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", style: { fontSize: 13, color: "#047857" }, children: pwState.message }) : null,
|
|
1346
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: changePassword, style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
|
|
1347
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "Current password", 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 }) }),
|
|
1348
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: "New password", 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 }) }),
|
|
1349
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PrimaryButton, { type: "submit", disabled: pwState.submitting || !oldPassword || !newPassword, children: pwState.submitting ? "Updating\u2026" : "Change password" })
|
|
1350
|
+
] })
|
|
1351
|
+
] }),
|
|
1352
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": "iqauth-sessions", children: [
|
|
1353
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: "iqauth-sessions", style: { fontSize: 14, fontWeight: 600 }, children: "Sessions" }),
|
|
1354
|
+
sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 13, opacity: 0.6 }, children: "No active sessions." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }, children: sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { display: "flex", justifyContent: "space-between", fontSize: 13, padding: "6px 10px", background: "#f8fafc", borderRadius: 6 }, children: [
|
|
1355
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: s.userAgent || s.deviceName || "Unknown" }),
|
|
1356
|
+
/* @__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: "Revoke" })
|
|
1357
|
+
] }, s.id)) })
|
|
1358
|
+
] })
|
|
1359
|
+
] });
|
|
1360
|
+
}
|
|
1361
|
+
function OrganizationSwitcher({ iqAuthBaseUrl, onSwitched, className }) {
|
|
1362
|
+
const [memberships, setMemberships] = (0, import_react.useState)([]);
|
|
1363
|
+
const [activeTenantId, setActiveTenantId] = (0, import_react.useState)(null);
|
|
1364
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1365
|
+
(0, import_react.useEffect)(() => {
|
|
1366
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/me`, { credentials: "include" }).then((r) => r.json()).then((p) => {
|
|
1367
|
+
if (p?.data?.tenantId) setActiveTenantId(p.data.tenantId);
|
|
1368
|
+
}).catch(() => {
|
|
1369
|
+
});
|
|
1370
|
+
fetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/tenants/memberships`, { credentials: "include" }).then((r) => r.json()).then((p) => setMemberships(p?.data?.memberships || p?.data || [])).catch(() => {
|
|
1371
|
+
});
|
|
1372
|
+
}, [iqAuthBaseUrl]);
|
|
1373
|
+
const switchTo = async (tenantId) => {
|
|
1374
|
+
try {
|
|
1375
|
+
await jsonFetch(`${iqAuthBaseUrl.replace(/\/$/, "")}/api/v1/auth/select-tenant`, {
|
|
1376
|
+
method: "POST",
|
|
1377
|
+
headers: { "Content-Type": "application/json" },
|
|
1378
|
+
body: JSON.stringify({ tenantId })
|
|
1379
|
+
});
|
|
1380
|
+
setActiveTenantId(tenantId);
|
|
1381
|
+
setOpen(false);
|
|
1382
|
+
onSwitched?.(tenantId);
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
const active = memberships.find((m) => m.tenantId === activeTenantId);
|
|
1387
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: { position: "relative", display: "inline-block" }, children: [
|
|
1388
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1389
|
+
"button",
|
|
1390
|
+
{
|
|
1391
|
+
type: "button",
|
|
1392
|
+
"aria-haspopup": "menu",
|
|
1393
|
+
"aria-expanded": open,
|
|
1394
|
+
onClick: () => setOpen((o) => !o),
|
|
1395
|
+
style: { background: "transparent", border: "1px solid rgba(15,23,42,0.15)", padding: "6px 12px", borderRadius: 6, cursor: "pointer", fontSize: 13 },
|
|
1396
|
+
children: active?.tenantName || active?.tenantSlug || "Select organization"
|
|
1397
|
+
}
|
|
1398
|
+
),
|
|
1399
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "menu", style: {
|
|
1400
|
+
position: "absolute",
|
|
1401
|
+
left: 0,
|
|
1402
|
+
top: 36,
|
|
1403
|
+
minWidth: 220,
|
|
1404
|
+
background: "#fff",
|
|
1405
|
+
border: "1px solid rgba(15,23,42,0.12)",
|
|
1406
|
+
borderRadius: 8,
|
|
1407
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
|
|
1408
|
+
padding: 8,
|
|
1409
|
+
zIndex: 100
|
|
1410
|
+
}, 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)(
|
|
1411
|
+
"button",
|
|
1412
|
+
{
|
|
1413
|
+
role: "menuitem",
|
|
1414
|
+
type: "button",
|
|
1415
|
+
onClick: () => switchTo(m.tenantId),
|
|
1416
|
+
style: {
|
|
1417
|
+
display: "block",
|
|
1418
|
+
width: "100%",
|
|
1419
|
+
textAlign: "left",
|
|
1420
|
+
padding: "8px 10px",
|
|
1421
|
+
background: m.tenantId === activeTenantId ? "rgba(99,102,241,0.08)" : "transparent",
|
|
1422
|
+
border: "none",
|
|
1423
|
+
borderRadius: 4,
|
|
1424
|
+
cursor: "pointer",
|
|
1425
|
+
fontSize: 13,
|
|
1426
|
+
color: "#0f172a"
|
|
1427
|
+
},
|
|
1428
|
+
children: [
|
|
1429
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 500 }, children: m.tenantName || m.tenantSlug || m.tenantId }),
|
|
1430
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, opacity: 0.6 }, children: m.roles.join(", ") })
|
|
1431
|
+
]
|
|
1432
|
+
},
|
|
1433
|
+
m.tenantId
|
|
1434
|
+
)) }) : null
|
|
1435
|
+
] });
|
|
1436
|
+
}
|
|
1437
|
+
var __version__ = "phase-bc-1.0.0";
|
|
1438
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1439
|
+
0 && (module.exports = {
|
|
1440
|
+
AuthCallback,
|
|
1441
|
+
IQAuthProvider,
|
|
1442
|
+
OrganizationSwitcher,
|
|
1443
|
+
RedirectToSignIn,
|
|
1444
|
+
SignIn,
|
|
1445
|
+
SignUp,
|
|
1446
|
+
SignedIn,
|
|
1447
|
+
SignedOut,
|
|
1448
|
+
UserButton,
|
|
1449
|
+
UserProfile,
|
|
1450
|
+
__version__,
|
|
1451
|
+
useAuth,
|
|
1452
|
+
useAuthFetch,
|
|
1453
|
+
useIQAuthSignInContext,
|
|
1454
|
+
useOrganization,
|
|
1455
|
+
useSession,
|
|
1456
|
+
useUser
|
|
1457
|
+
});
|