@object-ui/auth 17.5.0 → 17.7.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/CHANGELOG.md +600 -0
- package/README.md +245 -15
- package/dist/ActiveOrganizationStorage.d.ts +199 -0
- package/dist/ActiveOrganizationStorage.d.ts.map +1 -0
- package/dist/ActiveOrganizationStorage.js +584 -0
- package/dist/AuthContext.d.ts +23 -3
- package/dist/AuthContext.d.ts.map +1 -1
- package/dist/AuthGuard.d.ts +8 -1
- package/dist/AuthGuard.d.ts.map +1 -1
- package/dist/AuthGuard.js +16 -3
- package/dist/AuthProvider.d.ts +1 -1
- package/dist/AuthProvider.d.ts.map +1 -1
- package/dist/AuthProvider.js +274 -9
- package/dist/ForgotPasswordForm.d.ts +1 -1
- package/dist/ForgotPasswordForm.d.ts.map +1 -1
- package/dist/ForgotPasswordForm.js +5 -5
- package/dist/LoginForm.d.ts +11 -2
- package/dist/LoginForm.d.ts.map +1 -1
- package/dist/LoginForm.js +5 -6
- package/dist/PreviewBanner.js +1 -1
- package/dist/RegisterForm.d.ts +22 -3
- package/dist/RegisterForm.d.ts.map +1 -1
- package/dist/RegisterForm.js +6 -7
- package/dist/SocialSignInButtons.d.ts +9 -2
- package/dist/SocialSignInButtons.d.ts.map +1 -1
- package/dist/SocialSignInButtons.js +5 -5
- package/dist/UserMenu.d.ts +1 -1
- package/dist/UserMenu.js +3 -3
- package/dist/createAuthClient.d.ts +1 -1
- package/dist/createAuthClient.d.ts.map +1 -1
- package/dist/createAuthClient.js +42 -4
- package/dist/createAuthenticatedFetch.d.ts +9 -9
- package/dist/createAuthenticatedFetch.d.ts.map +1 -1
- package/dist/createAuthenticatedFetch.js +50 -38
- package/dist/index.d.ts +21 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -17
- package/dist/invitation-status.d.ts +59 -0
- package/dist/invitation-status.d.ts.map +1 -0
- package/dist/invitation-status.js +32 -0
- package/dist/types.d.ts +25 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/useAuth.d.ts +1 -1
- package/dist/useAuth.d.ts.map +1 -1
- package/dist/useAuth.js +5 -1
- package/dist/useWorkspaceAdminStatus.d.ts +134 -0
- package/dist/useWorkspaceAdminStatus.d.ts.map +1 -0
- package/dist/useWorkspaceAdminStatus.js +208 -0
- package/package.json +6 -7
- package/dist/useIsWorkspaceAdmin.d.ts +0 -21
- package/dist/useIsWorkspaceAdmin.d.ts.map +0 -1
- package/dist/useIsWorkspaceAdmin.js +0 -47
package/dist/AuthGuard.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthGuard.d.ts","sourceRoot":"","sources":["../src/AuthGuard.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B
|
|
1
|
+
{"version":3,"file":"AuthGuard.d.ts","sourceRoot":"","sources":["../src/AuthGuard.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,qCAAqC;IACrC,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,4CAA4C;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAe,EACf,aAAa,EACb,eAAe,EACf,QAAQ,GACT,EAAE,cAAc,qBAkChB"}
|
package/dist/AuthGuard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useAuth } from './useAuth';
|
|
2
|
+
import { useAuth } from './useAuth.js';
|
|
3
3
|
/**
|
|
4
4
|
* Route guard component that conditionally renders children
|
|
5
5
|
* based on authentication and authorization state.
|
|
@@ -23,9 +23,22 @@ export function AuthGuard({ fallback = null, requiredRoles, loadingFallback, chi
|
|
|
23
23
|
if (!isAuthenticated) {
|
|
24
24
|
return _jsx(_Fragment, { children: fallback });
|
|
25
25
|
}
|
|
26
|
-
// Check role requirements
|
|
26
|
+
// Check role requirements.
|
|
27
|
+
//
|
|
28
|
+
// `positions` is the ONE published spelling (framework ADR-0090 D3 renamed
|
|
29
|
+
// `roles` → `positions` with no deprecation window; maintainer ruling on
|
|
30
|
+
// objectui#5424). When the session carries the key — protocol 17 always
|
|
31
|
+
// emits it — it is the whole answer, empty included: falling back to the
|
|
32
|
+
// coarse `user.role` scalar when the server said "no positions" would be
|
|
33
|
+
// exactly the over-admission this gate used to have (`role: 'admin'`
|
|
34
|
+
// passing a gate meant for an `admin` position). The scalar fallback exists
|
|
35
|
+
// for identities that lack the key entirely: the guest user the provider
|
|
36
|
+
// seeds when auth is disabled, and pre-positions deployments. The retired
|
|
37
|
+
// `roles` spelling is deliberately NOT read — resurrecting it as a fallback
|
|
38
|
+
// dialect is what ADR-0090 D3 forbids
|
|
39
|
+
// (`__tests__/authGuardPositions-5424.test.tsx` pins the refusal).
|
|
27
40
|
if (requiredRoles && requiredRoles.length > 0 && user) {
|
|
28
|
-
const userRoles = user.
|
|
41
|
+
const userRoles = user.positions ?? (user.role ? [user.role] : []);
|
|
29
42
|
const hasRole = requiredRoles.some((role) => userRoles.includes(role));
|
|
30
43
|
if (!hasRole) {
|
|
31
44
|
return _jsx(_Fragment, { children: fallback });
|
package/dist/AuthProvider.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import type { AuthProviderOptions, PreviewModeOptions } from './types';
|
|
9
|
+
import type { AuthProviderOptions, PreviewModeOptions } from './types.js';
|
|
10
10
|
export interface AuthProviderProps extends AuthProviderOptions {
|
|
11
11
|
children: React.ReactNode;
|
|
12
12
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthProvider.d.ts","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,OAAO,KAAK,EAAwB,mBAAmB,EAAE,kBAAkB,EAAyG,MAAM,
|
|
1
|
+
{"version":3,"file":"AuthProvider.d.ts","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA4D,MAAM,OAAO,CAAC;AAEjF,OAAO,KAAK,EAAwB,mBAAmB,EAAE,kBAAkB,EAAyG,MAAM,YAAY,CAAC;AA4GvM,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,MAAM,EAAE,cAAc,EACtB,iBAAiB,EACjB,OAAc,EACd,WAAW,EACX,QAAQ,GACT,EAAE,iBAAiB,qBA43BnB"}
|
package/dist/AuthProvider.js
CHANGED
|
@@ -7,10 +7,112 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
|
-
import { authGateEvents } from './auth-gate-events';
|
|
11
|
-
import { AuthCtx } from './AuthContext';
|
|
12
|
-
import { createAuthClient, TokenStorage } from './createAuthClient';
|
|
13
|
-
import { ActiveOrganizationStorage } from './
|
|
10
|
+
import { authGateEvents } from './auth-gate-events.js';
|
|
11
|
+
import { AuthCtx } from './AuthContext.js';
|
|
12
|
+
import { createAuthClient, TokenStorage } from './createAuthClient.js';
|
|
13
|
+
import { ActiveOrganizationStorage, SessionUserScope } from './ActiveOrganizationStorage.js';
|
|
14
|
+
/**
|
|
15
|
+
* Prefix of every `MetadataProvider` seed entry in `sessionStorage`
|
|
16
|
+
* (`objectui:metadata:<type>:<orgId>:<principal>`, `@object-ui/app-shell`).
|
|
17
|
+
*
|
|
18
|
+
* Spelled out here rather than imported: `@object-ui/app-shell` depends on
|
|
19
|
+
* `@object-ui/auth`, so the constant cannot travel in that direction without a
|
|
20
|
+
* dependency cycle. The two ends are held together by BEHAVIOUR instead of by
|
|
21
|
+
* a shared symbol — `MetadataProvider.crossPrincipalSeed.test.tsx` fills the
|
|
22
|
+
* cache by running the real `MetadataProvider` and empties it by signing out
|
|
23
|
+
* through this provider, so a prefix that drifts on either side fails there
|
|
24
|
+
* rather than silently purging nothing.
|
|
25
|
+
*/
|
|
26
|
+
const METADATA_SEED_CACHE_PREFIX = 'objectui:metadata:';
|
|
27
|
+
/**
|
|
28
|
+
* Drop the client-side caches that belong to the session being ended
|
|
29
|
+
* (objectui#5198).
|
|
30
|
+
*
|
|
31
|
+
* `sessionStorage` is per-TAB, not per-session, and no sign-out call site
|
|
32
|
+
* reloads the page — `AppHeader`, `UserMenu` and `RemediationOverlay` all
|
|
33
|
+
* just call `signOut()` and let the SPA keep running.
|
|
34
|
+
* So without this the next person to sign in in the same tab (a shared or
|
|
35
|
+
* kiosk browser, a handover, a support session) is SEEDED from the previous
|
|
36
|
+
* user's entry, and what is in it is that user's PERMISSION-FILTERED app list —
|
|
37
|
+
* the server filters `GET /api/v1/meta/:type` per session. That makes it a
|
|
38
|
+
* cross-principal disclosure, not ordinary staleness, which is why it is
|
|
39
|
+
* cleared here and not left to the tab being closed.
|
|
40
|
+
*
|
|
41
|
+
* Two properties of the loop are load-bearing:
|
|
42
|
+
*
|
|
43
|
+
* - It matches by PREFIX. The keys are org-scoped (objectui#4486), so a purge
|
|
44
|
+
* that recomputed the scope would depend on `ActiveOrganizationStorage`
|
|
45
|
+
* still holding the org those entries were written under. A prefix sweep has
|
|
46
|
+
* no such dependency and therefore no ordering hazard of its own.
|
|
47
|
+
* - It runs BEFORE `ActiveOrganizationStorage.clear()` anyway, so the ordering
|
|
48
|
+
* stays correct for anything scope-derived added later: clearing the org
|
|
49
|
+
* first would leave such a purge computing the no-org scope and deleting
|
|
50
|
+
* nothing while the real entries survived.
|
|
51
|
+
*
|
|
52
|
+
* `MetadataProvider` additionally keys the entry by session fingerprint, so a
|
|
53
|
+
* blob that escapes this purge (a tab open across the upgrade, a session ended
|
|
54
|
+
* by expiry rather than by this call) is unreadable rather than merely
|
|
55
|
+
* undeleted. The two halves are deliberately independent.
|
|
56
|
+
*
|
|
57
|
+
* ## The `try` guards the SNAPSHOT, not the walk (objectui#5777)
|
|
58
|
+
*
|
|
59
|
+
* `Object.keys(sessionStorage)` can itself throw — partitioned iframes, some
|
|
60
|
+
* privacy modes — and that is the reason a guard exists here at all, so it
|
|
61
|
+
* stays. What it must NOT do is wrap the loop: a `removeItem` that throws on
|
|
62
|
+
* key `n` would abort the walk, leaving every still-unvisited MATCHING key
|
|
63
|
+
* unswept, with the failure swallowed so the caller believes the sweep
|
|
64
|
+
* completed. The keys this purges are a cross-principal disclosure risk on a
|
|
65
|
+
* shared browser (objectui#5198), not ordinary staleness — a partial sweep
|
|
66
|
+
* leaves an arbitrary subset of the previous principal's permission-filtered
|
|
67
|
+
* app list readable to whoever signs in next in this tab. So each
|
|
68
|
+
* `removeItem` gets its own `try`: one uncooperative key costs exactly that
|
|
69
|
+
* key. Same defect class and same remedy as `sweepStore` in
|
|
70
|
+
* `ActiveOrganizationStorage.ts` (objectui#5763) — different file and
|
|
71
|
+
* different caller (sign-out, not sign-in) is why it is fixed here rather
|
|
72
|
+
* than there.
|
|
73
|
+
*
|
|
74
|
+
* ## Reported, not quarantined — same reason as `sweepStore`
|
|
75
|
+
*
|
|
76
|
+
* This function walks keys it does not own reads for: `MetadataProvider`
|
|
77
|
+
* (`@object-ui/app-shell`) is the reader of the seed cache, not this
|
|
78
|
+
* provider — there is no local `get()` here to guard, so there is nothing to
|
|
79
|
+
* quarantine the way `ActiveOrganizationStorage.clear()` quarantines a key
|
|
80
|
+
* (objectui#5731). What IS mirrored is the reporting channel: a key whose
|
|
81
|
+
* `removeItem` throws is named in a `console.warn`, so a partial sweep is
|
|
82
|
+
* discoverable instead of silent. The caller (`signOut`, above) cannot act on
|
|
83
|
+
* it either way — the session is already ending — so, like `sweepStore` on
|
|
84
|
+
* the sign-in path, this must not throw.
|
|
85
|
+
*/
|
|
86
|
+
function purgeSignedOutClientCaches() {
|
|
87
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
88
|
+
let keys;
|
|
89
|
+
try {
|
|
90
|
+
// Snapshot the keys first (`Object.keys`) — removing entries during a
|
|
91
|
+
// live index walk shifts the ones behind it and skips half of them.
|
|
92
|
+
// Same idiom as the `MarketplacePackagePage` purge loop.
|
|
93
|
+
keys = Object.keys(sessionStorage);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
keys = []; /* storage unavailable */
|
|
97
|
+
}
|
|
98
|
+
const unswept = [];
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
if (!key.startsWith(METADATA_SEED_CACHE_PREFIX))
|
|
101
|
+
continue;
|
|
102
|
+
try {
|
|
103
|
+
sessionStorage.removeItem(key);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
unswept.push(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (unswept.length > 0) {
|
|
110
|
+
console.warn(`[purgeSignedOutClientCaches] could not remove ${unswept.length} key(s) from sessionStorage: ` +
|
|
111
|
+
`${unswept.join(', ')}. The signed-out user's metadata seed cache may still be readable under these keys.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
ActiveOrganizationStorage.clear();
|
|
115
|
+
}
|
|
14
116
|
/**
|
|
15
117
|
* Authentication context provider.
|
|
16
118
|
*
|
|
@@ -49,6 +151,12 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
49
151
|
const [activeOrganization, setActiveOrganization] = useState(null);
|
|
50
152
|
const [activeMember, setActiveMember] = useState(null);
|
|
51
153
|
const [isOrganizationsLoading, setIsOrganizationsLoading] = useState(false);
|
|
154
|
+
// objectui#5619 — the two halves of "the membership answer has landed".
|
|
155
|
+
// Kept as POSITIVE resolution flags rather than another `isLoading`: a
|
|
156
|
+
// loading flag reads `false` both before the request starts and after it
|
|
157
|
+
// finishes, and that is precisely the ambiguity this card exists to remove.
|
|
158
|
+
const [organizationsResolved, setOrganizationsResolved] = useState(false);
|
|
159
|
+
const [activeMemberResolved, setActiveMemberResolved] = useState(false);
|
|
52
160
|
// Determine if we're in preview mode
|
|
53
161
|
const isPreviewMode = previewMode != null;
|
|
54
162
|
// If auth is disabled or in preview mode, automatically set as authenticated
|
|
@@ -60,6 +168,19 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
60
168
|
// `getSession` is already reflected in the answer we are about to apply, and
|
|
61
169
|
// re-entering on it would loop this provider against the server forever.
|
|
62
170
|
const sessionLoadInFlight = useRef(false);
|
|
171
|
+
// objectui#5750 — the org id `switchOrganization` last resolved to, tracked
|
|
172
|
+
// independently of React state so a switch that starts before the PREVIOUS
|
|
173
|
+
// switch's render has committed still compares against a value, not a stale
|
|
174
|
+
// closure. Kept in sync with every path that can move `activeOrganization`
|
|
175
|
+
// (not just `switchOrganization` itself — `refreshOrganizations`'
|
|
176
|
+
// single-membership repair also does), via the effect below.
|
|
177
|
+
const activeOrgIdRef = useRef(null);
|
|
178
|
+
// Bumped by every `switchOrganization` call that decides to re-resolve
|
|
179
|
+
// identity. Lets an EARLIER switch's still-in-flight `getSession()` notice
|
|
180
|
+
// a LATER switch superseded it and discard its answer instead of applying
|
|
181
|
+
// stale, previous-organization data over the newer one (see
|
|
182
|
+
// `switchOrganization`).
|
|
183
|
+
const sessionLoadGeneration = useRef(0);
|
|
63
184
|
/**
|
|
64
185
|
* The ONE session loader. Runs on mount (below) and on every later
|
|
65
186
|
* re-resolution — `refreshSession` on the context, and the rotation
|
|
@@ -149,7 +270,13 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
149
270
|
email: 'preview@preview.local',
|
|
150
271
|
name,
|
|
151
272
|
role,
|
|
152
|
-
|
|
273
|
+
// `positions` is the published spelling (framework ADR-0090 D3); this
|
|
274
|
+
// line was the LAST producer of the retired `roles` key
|
|
275
|
+
// (objectui#5424, maintainer ruling 2026-08-22). Emitting the position
|
|
276
|
+
// keeps every `positions` consumer — `AuthGuard`'s `requiredRoles`,
|
|
277
|
+
// the approver-identity derivations, workspace-admin leg 3 — seeing a
|
|
278
|
+
// preview identity shaped like a protocol-17 session.
|
|
279
|
+
positions: [role],
|
|
153
280
|
});
|
|
154
281
|
setSession({
|
|
155
282
|
token: 'preview-token',
|
|
@@ -207,11 +334,11 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
207
334
|
setIsLoading(false);
|
|
208
335
|
}
|
|
209
336
|
}, [client]);
|
|
210
|
-
const signUp = useCallback(async (name, email, password) => {
|
|
337
|
+
const signUp = useCallback(async (name, email, password, callbackURL) => {
|
|
211
338
|
setIsLoading(true);
|
|
212
339
|
setError(null);
|
|
213
340
|
try {
|
|
214
|
-
const result = await client.signUp({ name, email, password });
|
|
341
|
+
const result = await client.signUp({ name, email, password, ...(callbackURL ? { callbackURL } : {}) });
|
|
215
342
|
if (result.requiresVerification) {
|
|
216
343
|
// No session was created — the user must verify their email before
|
|
217
344
|
// they can sign in. Leave auth state untouched so the caller can
|
|
@@ -238,11 +365,27 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
238
365
|
setUser(null);
|
|
239
366
|
setSession(null);
|
|
240
367
|
setError(null);
|
|
368
|
+
// The organization block is the previous principal's data too: the list
|
|
369
|
+
// is the workspaces THAT user belongs to (the switcher renders it), and
|
|
370
|
+
// a lingering `activeOrganization` would additionally suppress the
|
|
371
|
+
// re-resolution below — `refreshOrganizations` only asks the server for
|
|
372
|
+
// the active org `if (orgs.length > 0 && !activeOrganization)`, so the
|
|
373
|
+
// next sign-in in this tab would inherit the previous user's active
|
|
374
|
+
// workspace in context while storage had (correctly) forgotten it.
|
|
375
|
+
// `activeMember` follows from the effect that watches `activeOrganization`.
|
|
376
|
+
// Same pairing `deleteOrganization` / `leaveOrganization` already use in
|
|
377
|
+
// this file: drop the in-memory reference and the stored id together.
|
|
378
|
+
setOrganizations([]);
|
|
379
|
+
setActiveOrganization(null);
|
|
241
380
|
}
|
|
242
381
|
catch (err) {
|
|
243
382
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
244
383
|
}
|
|
245
384
|
finally {
|
|
385
|
+
// Unconditional: `createAuthClient.signOut` clears `TokenStorage` BEFORE
|
|
386
|
+
// it rethrows a server error, so a failed call still leaves this tab
|
|
387
|
+
// without a session — the caches it authenticated for must go with it.
|
|
388
|
+
purgeSignedOutClientCaches();
|
|
246
389
|
setIsLoading(false);
|
|
247
390
|
}
|
|
248
391
|
}, [client]);
|
|
@@ -453,6 +596,10 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
453
596
|
finally {
|
|
454
597
|
if (!isCancelled?.()) {
|
|
455
598
|
setIsOrganizationsLoading(false);
|
|
599
|
+
// In `finally`, so a failed `listOrganizations` still counts as an
|
|
600
|
+
// answer. It is not a good answer, but leaving it unresolved would
|
|
601
|
+
// strand every gate that waits on it (objectui#5619).
|
|
602
|
+
setOrganizationsResolved(true);
|
|
456
603
|
}
|
|
457
604
|
}
|
|
458
605
|
}, [client, enabled, isPreviewMode, activeOrganization]);
|
|
@@ -468,12 +615,25 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
468
615
|
userId: 'preview-user',
|
|
469
616
|
role,
|
|
470
617
|
});
|
|
618
|
+
// Neither mode runs `refreshOrganizations` (its effect requires a real
|
|
619
|
+
// `user`, and the callback itself returns early), so this is the one
|
|
620
|
+
// place that can close BOTH halves for them (objectui#5619).
|
|
621
|
+
setOrganizationsResolved(true);
|
|
622
|
+
setActiveMemberResolved(true);
|
|
471
623
|
return;
|
|
472
624
|
}
|
|
473
625
|
if (!activeOrganization) {
|
|
626
|
+
// "No active organization" is a real answer about the member row — but
|
|
627
|
+
// only once the org list itself has come back, which is what
|
|
628
|
+
// `organizationsResolved` below contributes. On mount this branch runs
|
|
629
|
+
// with the pipeline not yet started, and the combined flag stays false.
|
|
474
630
|
setActiveMember(null);
|
|
631
|
+
setActiveMemberResolved(true);
|
|
475
632
|
return;
|
|
476
633
|
}
|
|
634
|
+
// A switch re-opens the question: until `getActiveMember()` answers for the
|
|
635
|
+
// NEW org, `activeMember` still holds the OLD org's row.
|
|
636
|
+
setActiveMemberResolved(false);
|
|
477
637
|
try {
|
|
478
638
|
const member = await client.getActiveMember();
|
|
479
639
|
setActiveMember(member);
|
|
@@ -482,7 +642,42 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
482
642
|
console.warn('[AuthProvider] Failed to load active member:', err);
|
|
483
643
|
setActiveMember(null);
|
|
484
644
|
}
|
|
645
|
+
finally {
|
|
646
|
+
setActiveMemberResolved(true);
|
|
647
|
+
}
|
|
485
648
|
}, [client, enabled, isPreviewMode, previewMode, activeOrganization]);
|
|
649
|
+
/**
|
|
650
|
+
* objectui#5664 — the session-user-change invariant.
|
|
651
|
+
*
|
|
652
|
+
* Declared BEFORE the two effects below on purpose: effects run in the order
|
|
653
|
+
* their hooks were called, so this settles "whose browser is this" before
|
|
654
|
+
* `refreshActiveMember` and `refreshOrganizations` read or write anything
|
|
655
|
+
* org-scoped. `refreshOrganizations` in particular calls
|
|
656
|
+
* `ActiveOrganizationStorage.set()`, which resolves its key through the
|
|
657
|
+
* pointer this establishes.
|
|
658
|
+
*
|
|
659
|
+
* Keyed on the session user ID rather than wired into each of the five
|
|
660
|
+
* places that set `user` (mount / `refreshSession` / rotation, sign-in,
|
|
661
|
+
* sign-up, and the two provider flows). One implementation, and a sign-in
|
|
662
|
+
* path added later is covered without being told to be.
|
|
663
|
+
*
|
|
664
|
+
* Preview and auth-disabled mounts are excluded because their identities are
|
|
665
|
+
* SYNTHETIC — a fixed `preview-user` / `guest` id. Adopting one would read as
|
|
666
|
+
* a user change and purge a real user's state on any browser that opened a
|
|
667
|
+
* marketplace demo.
|
|
668
|
+
*
|
|
669
|
+
* What "purge" means, and why it is wholesale rather than key-by-key, is in
|
|
670
|
+
* `ActiveOrganizationStorage.ts`: it is an allowlist sweep, so the NEXT
|
|
671
|
+
* un-namespaced key is covered before anyone writes it.
|
|
672
|
+
*/
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
if (!enabled || isPreviewMode)
|
|
675
|
+
return;
|
|
676
|
+
const sessionUserId = user?.id;
|
|
677
|
+
if (!sessionUserId)
|
|
678
|
+
return;
|
|
679
|
+
SessionUserScope.adopt(sessionUserId);
|
|
680
|
+
}, [user?.id, enabled, isPreviewMode]);
|
|
486
681
|
useEffect(() => {
|
|
487
682
|
refreshActiveMember();
|
|
488
683
|
}, [refreshActiveMember]);
|
|
@@ -494,8 +689,46 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
494
689
|
refreshOrganizations(() => cancelled);
|
|
495
690
|
return () => { cancelled = true; };
|
|
496
691
|
}, [user, enabled, isPreviewMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
692
|
+
// objectui#5750 — keep `activeOrgIdRef` current for every path that can
|
|
693
|
+
// move `activeOrganization`, not only `switchOrganization` (e.g.
|
|
694
|
+
// `refreshOrganizations`' single-membership repair calls
|
|
695
|
+
// `client.setActiveOrganization` directly). `switchOrganization` also
|
|
696
|
+
// writes it synchronously on its own success, below — this effect is the
|
|
697
|
+
// fallback for every OTHER path, and re-affirms after commit.
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
activeOrgIdRef.current = activeOrganization?.id ?? null;
|
|
700
|
+
}, [activeOrganization]);
|
|
701
|
+
/**
|
|
702
|
+
* objectui#5750 — `switchOrganization` now DECLARES the re-resolution
|
|
703
|
+
* instead of relying on the objectui#4467 rotation subscription noticing
|
|
704
|
+
* one by accident.
|
|
705
|
+
*
|
|
706
|
+
* Why that accident cannot be trusted here: `POST /organization/set-active`
|
|
707
|
+
* always returns the SIGNED `token.signature` spelling in `set-auth-token`
|
|
708
|
+
* (better-auth's `bearer()` fires for any response that stages a session
|
|
709
|
+
* cookie), which differs from the UNSIGNED `session.token` spelling the
|
|
710
|
+
* client normally holds — so `TokenStorage.set` reads the flip as a
|
|
711
|
+
* rotation and the objectui#4467 subscription re-resolves identity. That
|
|
712
|
+
* accidental rotation is deterministic on the RAW token, not on the
|
|
713
|
+
* organization — so a SECOND switch whose response lands before the FIRST
|
|
714
|
+
* switch's follow-up `get-session` has re-armed it (re-stored the unsigned
|
|
715
|
+
* spelling) writes the SAME signed value already in storage. No value
|
|
716
|
+
* change, no notification, no re-resolution — `user.positions` is left
|
|
717
|
+
* answering for the organization the FIRST switch targeted, while
|
|
718
|
+
* `activeOrganization` already reads as the new one. Measured in #5749/#5750.
|
|
719
|
+
*
|
|
720
|
+
* The fix does not add a second call on the common path: it SUPPRESSES the
|
|
721
|
+
* accidental one (via `sessionLoadInFlight`, the same flag that already
|
|
722
|
+
* suppresses the rotation observed inside `loadSession`'s own
|
|
723
|
+
* `getSession()` call) and re-resolves explicitly instead, so a switch that
|
|
724
|
+
* changes the active organization still spends exactly one `get-session` —
|
|
725
|
+
* now unconditionally, so it does not depend on which spelling happened to
|
|
726
|
+
* already be in storage.
|
|
727
|
+
*/
|
|
497
728
|
const switchOrganization = useCallback(async (orgId) => {
|
|
498
729
|
setError(null);
|
|
730
|
+
const previousOrgId = activeOrgIdRef.current;
|
|
731
|
+
sessionLoadInFlight.current = true;
|
|
499
732
|
try {
|
|
500
733
|
const org = await client.setActiveOrganization(orgId);
|
|
501
734
|
setActiveOrganization(org);
|
|
@@ -506,13 +739,28 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
506
739
|
else {
|
|
507
740
|
ActiveOrganizationStorage.clear();
|
|
508
741
|
}
|
|
742
|
+
const newOrgId = org?.id ?? null;
|
|
743
|
+
if (newOrgId !== previousOrgId) {
|
|
744
|
+
activeOrgIdRef.current = newOrgId;
|
|
745
|
+
// A LATER switch may already be in flight by the time this
|
|
746
|
+
// `getSession()` answer comes back (no debounce guards repeat
|
|
747
|
+
// switches — see the WorkspaceSwitcher/OrganizationLayout callers).
|
|
748
|
+
// Discard this answer if a newer switch has since claimed the
|
|
749
|
+
// generation, rather than clobber fresher, later-organization data
|
|
750
|
+
// with a stale, earlier one.
|
|
751
|
+
const generation = ++sessionLoadGeneration.current;
|
|
752
|
+
await loadSession(() => sessionLoadGeneration.current !== generation);
|
|
753
|
+
}
|
|
509
754
|
}
|
|
510
755
|
catch (err) {
|
|
511
756
|
const authError = err instanceof Error ? err : new Error(String(err));
|
|
512
757
|
setError(authError);
|
|
513
758
|
throw authError;
|
|
514
759
|
}
|
|
515
|
-
|
|
760
|
+
finally {
|
|
761
|
+
sessionLoadInFlight.current = false;
|
|
762
|
+
}
|
|
763
|
+
}, [client, loadSession]);
|
|
516
764
|
const createOrganization = useCallback(async (data) => {
|
|
517
765
|
setError(null);
|
|
518
766
|
try {
|
|
@@ -576,6 +824,22 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
576
824
|
// Guest mode (enabled=false) and preview mode both have hardcoded `isAuthenticated`
|
|
577
825
|
// and a no-op signOut backend, so consumers should hide sign-out UIs.
|
|
578
826
|
const isAuthEnabled = enabled && !isPreviewMode;
|
|
827
|
+
// objectui#5619 — see `AuthContext.isMembershipResolved` for the contract.
|
|
828
|
+
//
|
|
829
|
+
// Three modes, three reasons:
|
|
830
|
+
// - preview / auth-disabled: no pipeline runs; `refreshActiveMember`'s first
|
|
831
|
+
// branch closes both halves on its own effect.
|
|
832
|
+
// - no user: there is no membership to fetch, so the question is answered.
|
|
833
|
+
// Without this the flag would sit false forever on an unauthenticated
|
|
834
|
+
// visitor and hang any gate that waits on it. Consumers that must also
|
|
835
|
+
// wait for the SESSION read `isLoading` alongside this (the hook does).
|
|
836
|
+
// - signed in: both halves, because "no active organization" only means
|
|
837
|
+
// "no member row" once the org list has actually come back.
|
|
838
|
+
const isMembershipResolved = !isAuthEnabled
|
|
839
|
+
? activeMemberResolved
|
|
840
|
+
: user == null
|
|
841
|
+
? true
|
|
842
|
+
: organizationsResolved && activeMemberResolved;
|
|
579
843
|
const value = useMemo(() => ({
|
|
580
844
|
user,
|
|
581
845
|
session,
|
|
@@ -611,6 +875,7 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
611
875
|
activeOrganization,
|
|
612
876
|
activeMember,
|
|
613
877
|
isOrganizationsLoading,
|
|
878
|
+
isMembershipResolved,
|
|
614
879
|
switchOrganization,
|
|
615
880
|
createOrganization,
|
|
616
881
|
refreshOrganizations,
|
|
@@ -633,7 +898,7 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
|
|
|
633
898
|
signIn, signUp, signOut, refreshSession, updateUser, forgotPassword, sendVerificationEmail, resetPassword, changePassword, setInitialPassword, hasLocalPassword, getAuthConfig, signInWithProvider,
|
|
634
899
|
sendPhoneOtp, signInWithPhoneOtp, signInWithPhonePassword, requestPhonePasswordReset, resetPasswordWithPhoneOtp,
|
|
635
900
|
remediationRequired, enrollTotp, verifyTotp,
|
|
636
|
-
organizations, activeOrganization, activeMember, isOrganizationsLoading, switchOrganization, createOrganization, refreshOrganizations,
|
|
901
|
+
organizations, activeOrganization, activeMember, isOrganizationsLoading, isMembershipResolved, switchOrganization, createOrganization, refreshOrganizations,
|
|
637
902
|
updateOrganization, deleteOrganization, leaveOrganization,
|
|
638
903
|
getMembers, inviteMember, describeDelegableScope, removeMember, updateMemberRole,
|
|
639
904
|
listInvitations, cancelInvitation, getInvitation, acceptInvitation, rejectInvitation, listUserInvitations,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import type { AuthLinkComponentProps } from './types';
|
|
9
|
+
import type { AuthLinkComponentProps } from './types.js';
|
|
10
10
|
/** Translatable labels for the ForgotPasswordForm */
|
|
11
11
|
export interface ForgotPasswordFormLabels {
|
|
12
12
|
emailLabel?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ForgotPasswordForm.d.ts","sourceRoot":"","sources":["../src/ForgotPasswordForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ForgotPasswordForm.d.ts","sourceRoot":"","sources":["../src/ForgotPasswordForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAYzD,qDAAqD;AACrD,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gDAAgD;IAChD,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,wBAAwB;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,OAAO,EACP,QAAmB,EACnB,KAA6B,EAC7B,WAAyF,EACzF,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,GACZ,EAAE,uBAAuB,qBA0UzB"}
|
|
@@ -7,8 +7,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useState } from 'react';
|
|
10
|
-
import { useAuth } from './useAuth';
|
|
11
|
-
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthMailIcon, AuthSpinner, } from './authStyles';
|
|
10
|
+
import { useAuth } from './useAuth.js';
|
|
11
|
+
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthMailIcon, AuthSpinner, } from './authStyles.js';
|
|
12
12
|
const DefaultLink = ({ href, className, children }) => (_jsx("a", { href: href, className: className, children: children }));
|
|
13
13
|
const DefaultKeyIcon = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className: "h-6 w-6", "aria-hidden": "true", children: _jsx("path", { d: "M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" }) }));
|
|
14
14
|
/**
|
|
@@ -72,7 +72,7 @@ export function ForgotPasswordForm({ onSuccess, onError, loginUrl = '/login', ti
|
|
|
72
72
|
submittingButton: labels.submittingButton ?? 'Sending…',
|
|
73
73
|
successTitle: labels.successTitle ?? 'Check your email',
|
|
74
74
|
successDescription: labels.successDescription ??
|
|
75
|
-
"We've sent a password reset link to {
|
|
75
|
+
"We've sent a password reset link to {email}. Please check your inbox.",
|
|
76
76
|
backToSignInText: labels.backToSignInText ?? 'Back to sign in',
|
|
77
77
|
rememberPasswordText: labels.rememberPasswordText ?? 'Remember your password?',
|
|
78
78
|
signInText: labels.signInText ?? 'Sign in',
|
|
@@ -156,8 +156,8 @@ export function ForgotPasswordForm({ onSuccess, onError, loginUrl = '/login', ti
|
|
|
156
156
|
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: _jsx("span", { className: "flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/10 text-emerald-600 ring-1 ring-emerald-500/15 dark:text-emerald-400", children: _jsx(AuthMailIcon, {}) }), title: l.phoneSuccessTitle, description: l.phoneSuccessDescription }), loginUrl && (_jsx("p", { className: "text-center text-sm text-muted-foreground", children: _jsxs(LinkComp, { href: loginUrl, className: AUTH_LINK_CLASS, children: ["\u2190 ", l.backToSignInText] }) }))] }));
|
|
157
157
|
}
|
|
158
158
|
if (submitted) {
|
|
159
|
-
const successMsg = l.successDescription.includes('{
|
|
160
|
-
? l.successDescription.replace('{
|
|
159
|
+
const successMsg = l.successDescription.includes('{email}')
|
|
160
|
+
? l.successDescription.replace('{email}', email)
|
|
161
161
|
: `${l.successDescription} ${email}`;
|
|
162
162
|
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: _jsx("span", { className: "flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/10 text-emerald-600 ring-1 ring-emerald-500/15 dark:text-emerald-400", children: _jsx(AuthMailIcon, {}) }), title: l.successTitle, description: successMsg }), loginUrl && (_jsx("p", { className: "text-center text-sm text-muted-foreground", children: _jsxs(LinkComp, { href: loginUrl, className: AUTH_LINK_CLASS, children: ["\u2190 ", l.backToSignInText] }) }))] }));
|
|
163
163
|
}
|
package/dist/LoginForm.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import type { AuthLinkComponentProps } from './types';
|
|
9
|
+
import type { AuthLinkComponentProps } from './types.js';
|
|
10
10
|
/** Translatable labels for the LoginForm */
|
|
11
11
|
export interface LoginFormLabels {
|
|
12
12
|
emailLabel?: string;
|
|
@@ -22,8 +22,17 @@ export interface LoginFormLabels {
|
|
|
22
22
|
submittingButton?: string;
|
|
23
23
|
noAccountText?: string;
|
|
24
24
|
signUpText?: string;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Divider label between the social provider buttons and the email/password
|
|
27
|
+
* form (defaults to "or continue with email"). Rendered only when the server
|
|
28
|
+
* reports at least one social provider.
|
|
29
|
+
*/
|
|
26
30
|
orText?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Social provider button label (defaults to "Continue with {provider}");
|
|
33
|
+
* `{provider}` is replaced with the provider's display name.
|
|
34
|
+
*/
|
|
35
|
+
socialButton?: string;
|
|
27
36
|
/** Label for the SSO sign-in button (defaults to "Sign in with SSO") */
|
|
28
37
|
ssoButton?: string;
|
|
29
38
|
/**
|
package/dist/LoginForm.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoginForm.d.ts","sourceRoot":"","sources":["../src/LoginForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"LoginForm.d.ts","sourceRoot":"","sources":["../src/LoginForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAWzD,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8GAA8G;IAC9G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yHAAyH;IACzH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,OAAO,EACP,WAAyB,EACzB,iBAAsC,EACtC,KAAiC,EACjC,WAAyD,EACzD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,cAAc,qBAsbhB"}
|
package/dist/LoginForm.js
CHANGED
|
@@ -7,10 +7,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useState } from 'react';
|
|
10
|
-
import { useAuth } from './useAuth';
|
|
11
|
-
import { SocialSignInButtons } from './SocialSignInButtons';
|
|
12
|
-
import { looksLikePhoneIdentifier, normalizePhoneIdentifier } from './phone-identifier';
|
|
13
|
-
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthSpinner, } from './authStyles';
|
|
10
|
+
import { useAuth } from './useAuth.js';
|
|
11
|
+
import { SocialSignInButtons } from './SocialSignInButtons.js';
|
|
12
|
+
import { looksLikePhoneIdentifier, normalizePhoneIdentifier } from './phone-identifier.js';
|
|
13
|
+
import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthSpinner, } from './authStyles.js';
|
|
14
14
|
const DefaultLink = ({ href, className, children }) => (_jsx("a", { href: href, className: className, children: children }));
|
|
15
15
|
const DefaultLockIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className: "h-6 w-6", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] }));
|
|
16
16
|
/**
|
|
@@ -117,7 +117,6 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
|
|
|
117
117
|
submittingButton: labels.submittingButton ?? 'Signing in…',
|
|
118
118
|
noAccountText: labels.noAccountText ?? "Don't have an account?",
|
|
119
119
|
signUpText: labels.signUpText ?? 'Sign up',
|
|
120
|
-
orText: labels.orText ?? 'or',
|
|
121
120
|
ssoButton: labels.ssoButton ?? 'Sign in with SSO',
|
|
122
121
|
usePasswordText: labels.usePasswordText ?? 'Use a password instead',
|
|
123
122
|
backToSsoText: labels.backToSsoText ?? 'Back to single sign-on',
|
|
@@ -234,7 +233,7 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
|
|
|
234
233
|
setSsoSubmitting(false);
|
|
235
234
|
}
|
|
236
235
|
};
|
|
237
|
-
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultLockIcon, {})), title: title, description: ssoEnforced && !showPasswordFallback ? l.ssoOnlyDescription : description }), _jsx("div", { className: "space-y-5", children: configPending ? (_jsx("div", { className: "flex justify-center py-10", role: "status", "aria-live": "polite", "data-testid": "login-config-loading", children: _jsx(AuthSpinner, {}) })) : (_jsxs(_Fragment, { children: [_jsx(SocialSignInButtons, { mode: "sign-in", onProvidersResolved: (hasProviders) => setHasSocialProviders(hasProviders) }), passwordFormVisible ? (mode === 'phone-otp' ? (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-phone", className: AUTH_FIELD_LABEL_CLASS, children: l.phoneLabel }), _jsx("input", { id: "login-phone", type: "tel", placeholder: l.phonePlaceholder, value: phone, onChange: (e) => setPhone(e.target.value), required: true, autoComplete: "tel", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-otp", className: AUTH_FIELD_LABEL_CLASS, children: l.otpCodeLabel }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { id: "login-otp", type: "text", inputMode: "numeric", placeholder: l.otpCodePlaceholder, value: otpCode, onChange: (e) => setOtpCode(e.target.value), required: true, autoComplete: "one-time-code", disabled: isLoading, className: AUTH_INPUT_CLASS }), _jsxs("button", { type: "button", onClick: handleSendOtp, disabled: otpSending || otpCooldown > 0 || !phone.trim() || isLoading, className: "shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [otpSending && _jsx(AuthSpinner, {}), otpCooldown > 0
|
|
236
|
+
return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultLockIcon, {})), title: title, description: ssoEnforced && !showPasswordFallback ? l.ssoOnlyDescription : description }), _jsx("div", { className: "space-y-5", children: configPending ? (_jsx("div", { className: "flex justify-center py-10", role: "status", "aria-live": "polite", "data-testid": "login-config-loading", children: _jsx(AuthSpinner, {}) })) : (_jsxs(_Fragment, { children: [_jsx(SocialSignInButtons, { mode: "sign-in", dividerText: labels.orText, buttonText: labels.socialButton, onProvidersResolved: (hasProviders) => setHasSocialProviders(hasProviders) }), passwordFormVisible ? (mode === 'phone-otp' ? (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-phone", className: AUTH_FIELD_LABEL_CLASS, children: l.phoneLabel }), _jsx("input", { id: "login-phone", type: "tel", placeholder: l.phonePlaceholder, value: phone, onChange: (e) => setPhone(e.target.value), required: true, autoComplete: "tel", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-otp", className: AUTH_FIELD_LABEL_CLASS, children: l.otpCodeLabel }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { id: "login-otp", type: "text", inputMode: "numeric", placeholder: l.otpCodePlaceholder, value: otpCode, onChange: (e) => setOtpCode(e.target.value), required: true, autoComplete: "one-time-code", disabled: isLoading, className: AUTH_INPUT_CLASS }), _jsxs("button", { type: "button", onClick: handleSendOtp, disabled: otpSending || otpCooldown > 0 || !phone.trim() || isLoading, className: "shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [otpSending && _jsx(AuthSpinner, {}), otpCooldown > 0
|
|
238
237
|
? l.resendOtpCountdownText.replace('{seconds}', String(otpCooldown))
|
|
239
238
|
: l.sendOtpButton] })] })] }), _jsxs("button", { type: "submit", disabled: isLoading || !otpSent, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] }), _jsx("button", { type: "button", onClick: () => switchMode('password'), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePasswordSignInText })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-email", className: AUTH_FIELD_LABEL_CLASS, children: phonePasswordEnabled ? l.emailOrPhoneLabel : l.emailLabel }), _jsx("input", { id: "login-email",
|
|
240
239
|
// `text` (not `email`) when a phone number is also accepted, so the
|
package/dist/PreviewBanner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useAuth } from './useAuth';
|
|
2
|
+
import { useAuth } from './useAuth.js';
|
|
3
3
|
/**
|
|
4
4
|
* Banner component that displays a message when the app is in preview mode.
|
|
5
5
|
* Only renders when preview mode is active. Uses the bannerMessage from
|