@rebasepro/app 0.13.1-canary.gf57a27e → 0.14.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/dist/components/LoginView/oauth-redirect-flow.d.ts +86 -0
- package/dist/hooks/data/useRelationSelector.d.ts +12 -1
- package/dist/index.es.js +208 -47
- package/dist/index.es.js.map +1 -1
- package/dist/util/icons.d.ts +5 -1
- package/package.json +7 -7
- package/src/auth/useRebaseAuthController.ts +6 -0
- package/src/components/LoginView/LoginView.tsx +68 -31
- package/src/components/LoginView/oauth-redirect-flow.ts +184 -0
- package/src/hooks/data/useRelationSelector.tsx +26 -4
- package/src/locales/de.ts +1 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/hi.ts +1 -0
- package/src/locales/it.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/util/icons.tsx +10 -30
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the OAuth authorization-code flow.
|
|
3
|
+
*
|
|
4
|
+
* The server only ever sees a `code` and a `redirectUri` taken from the
|
|
5
|
+
* request body, so `state`, PKCE and the binding between "this browser started
|
|
6
|
+
* a login" and "this code came back" are, by construction, the client's job.
|
|
7
|
+
* In the admin login they had been nobody's job: the authorize URL carried no
|
|
8
|
+
* `state`, and the return leg accepted any `?code=` as long as a localStorage
|
|
9
|
+
* marker naming the *provider* was set — a marker written on button click and
|
|
10
|
+
* removed only on a callback that carried a code, so abandoning the flow left
|
|
11
|
+
* it set indefinitely.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is provider-agnostic on purpose. A button that wants a new
|
|
14
|
+
* provider calls {@link startOAuthRedirect} and gets state, expiry and PKCE
|
|
15
|
+
* without deciding anything, which is what stops the next provider shipping
|
|
16
|
+
* without them.
|
|
17
|
+
*/
|
|
18
|
+
export interface OAuthRedirectRequest {
|
|
19
|
+
/** Provider id, as mounted at `POST /auth/<id>`. */
|
|
20
|
+
provider: string;
|
|
21
|
+
/** The provider's authorization endpoint. */
|
|
22
|
+
authorizeUrl: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
scope: string;
|
|
25
|
+
/** Where the provider sends the browser back. Echoed to the token exchange. */
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
/** Set when the provider implements PKCE. */
|
|
28
|
+
pkce?: boolean;
|
|
29
|
+
/** Extra authorize-endpoint parameters (`response_type`, and so on). */
|
|
30
|
+
params?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/** What the browser remembers between leaving for the provider and coming back. */
|
|
33
|
+
export interface PendingOAuthRedirect {
|
|
34
|
+
provider: string;
|
|
35
|
+
state: string;
|
|
36
|
+
codeVerifier?: string;
|
|
37
|
+
redirectUri: string;
|
|
38
|
+
startedAt: number;
|
|
39
|
+
}
|
|
40
|
+
export type OAuthCallbackResult =
|
|
41
|
+
/** No OAuth callback in this URL. */
|
|
42
|
+
{
|
|
43
|
+
status: "none";
|
|
44
|
+
}
|
|
45
|
+
/** The provider reported a failure, or the user declined. */
|
|
46
|
+
| {
|
|
47
|
+
status: "error";
|
|
48
|
+
error: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A code arrived that this browser cannot account for: no pending
|
|
52
|
+
* authorization, an expired one, or a `state` that does not match. This is
|
|
53
|
+
* what a login-CSRF / code-injection attempt looks like.
|
|
54
|
+
*/
|
|
55
|
+
| {
|
|
56
|
+
status: "mismatch";
|
|
57
|
+
} | {
|
|
58
|
+
status: "ok";
|
|
59
|
+
provider: string;
|
|
60
|
+
code: string;
|
|
61
|
+
codeVerifier?: string;
|
|
62
|
+
redirectUri: string;
|
|
63
|
+
};
|
|
64
|
+
type Storage = Pick<globalThis.Storage, "getItem" | "setItem" | "removeItem">;
|
|
65
|
+
export declare function readPendingOAuthRedirect(storage?: Storage | null): PendingOAuthRedirect | null;
|
|
66
|
+
export declare function clearPendingOAuthRedirect(storage?: Storage | null): void;
|
|
67
|
+
/**
|
|
68
|
+
* Build the authorize URL and the pending record that will validate its
|
|
69
|
+
* callback. Split out from {@link startOAuthRedirect} so it can be tested
|
|
70
|
+
* without navigating.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildOAuthAuthorization(request: OAuthRedirectRequest, now?: number): Promise<{
|
|
73
|
+
url: string;
|
|
74
|
+
pending: PendingOAuthRedirect;
|
|
75
|
+
}>;
|
|
76
|
+
/** Remember the authorization and send the browser to the provider. */
|
|
77
|
+
export declare function startOAuthRedirect(request: OAuthRedirectRequest, storage?: Storage | null): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Interpret a return from the provider.
|
|
80
|
+
*
|
|
81
|
+
* Clears the pending record on **every** path that saw a callback — including
|
|
82
|
+
* `?error=` and a state mismatch — so a declined or abandoned consent screen
|
|
83
|
+
* cannot leave the browser primed to accept somebody else's code later.
|
|
84
|
+
*/
|
|
85
|
+
export declare function consumeOAuthCallback(search: string, storage?: Storage | null, now?: number): OAuthCallbackResult;
|
|
86
|
+
export {};
|
|
@@ -36,6 +36,17 @@ export interface UseRelationSelectorProps<M extends Record<string, any> = any> {
|
|
|
36
36
|
* Property name to use as the secondary display field
|
|
37
37
|
*/
|
|
38
38
|
descriptionProperty?: keyof M;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the list should be fetched at all. Defaults to `true`.
|
|
41
|
+
*
|
|
42
|
+
* A picker that is mounted is not a picker that is open, and the two used
|
|
43
|
+
* to be the same thing here: the fetch ran on mount, so a collection table
|
|
44
|
+
* with a relation column paid for one query — or one realtime subscription
|
|
45
|
+
* — per rendered row before anyone clicked a cell. Pass `false` until the
|
|
46
|
+
* list is actually needed and nothing is requested; flipping it to `true`
|
|
47
|
+
* fetches once, and it never goes back.
|
|
48
|
+
*/
|
|
49
|
+
enabled?: boolean;
|
|
39
50
|
}
|
|
40
51
|
export interface RelationSelectorController {
|
|
41
52
|
items: RelationItem[];
|
|
@@ -49,4 +60,4 @@ export interface RelationSelectorController {
|
|
|
49
60
|
/**
|
|
50
61
|
* Hook to manage relation selection with data fetching from Rebase data source
|
|
51
62
|
*/
|
|
52
|
-
export declare function useRelationSelector<M extends Record<string, any> = any>({ path, collection, fixedFilter, pageSize, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty }: UseRelationSelectorProps<M>): RelationSelectorController;
|
|
63
|
+
export declare function useRelationSelector<M extends Record<string, any> = any>({ path, collection, fixedFilter, pageSize, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty, enabled }: UseRelationSelectorProps<M>): RelationSelectorController;
|
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, lazy, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Alert, AlertCircleIcon, AlertTriangleIcon, AppWindow, ArrowDownIcon, ArrowLeftIcon, ArrowRightLeftIcon, ArrowUpDownIcon, ArrowUpIcon, Avatar, BooleanSwitch, Button, CalendarIcon, Card, CenteredView, CheckCircle2Icon, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, Chip, CircleDotIcon, CircleUserIcon, CircularProgress, ColumnsIcon, Container, Dialog, DialogActions, DialogContent, DialogTitle, DollarSignIcon, ErrorBoundary, FileIcon, FileTextIcon, FilterChip, FilterIcon, FolderIcon, FolderKanbanIcon, IconButton, KanbanIcon, KanbanView, LanguagesIcon, LayoutGridIcon, ListIcon, ListPlusIcon, ListTodoIcon, LoadingButton, LogOutIcon, MailIcon, Menu, MenuItem, MessageCircleIcon, MoonIcon, MultiSelect, MultiSelectItem, PanelLeftIcon, Paper, PenLineIcon, PencilIcon, PhoneIcon, PinIcon, PlusIcon, Popover, RefreshCwIcon, SearchBar, Select, SelectItem, Separator, SettingsIcon, Skeleton, StickyNoteIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableRow, Tabs, TagIcon, TextField, ToggleButtonGroup, Tooltip, Trash2Icon, TrendingUpIcon, TypeIcon, Typography, UserIcon, UserPlus, UsersIcon, VideoIcon, Wand2Icon, WrenchIcon, XIcon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize
|
|
2
|
+
import { Alert, AlertCircleIcon, AlertTriangleIcon, AppWindow, ArrowDownIcon, ArrowLeftIcon, ArrowRightLeftIcon, ArrowUpDownIcon, ArrowUpIcon, Avatar, BooleanSwitch, Button, CalendarIcon, Card, CenteredView, CheckCircle2Icon, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, Chip, CircleDotIcon, CircleUserIcon, CircularProgress, ColumnsIcon, Container, Dialog, DialogActions, DialogContent, DialogTitle, DollarSignIcon, ErrorBoundary, FileIcon, FileTextIcon, FilterChip, FilterIcon, FolderIcon, FolderKanbanIcon, IconButton, KanbanIcon, KanbanView, LanguagesIcon, LayoutGridIcon, ListIcon, ListPlusIcon, ListTodoIcon, LoadingButton, LogOutIcon, LucideIconByName, MailIcon, Menu, MenuItem, MessageCircleIcon, MoonIcon, MultiSelect, MultiSelectItem, PanelLeftIcon, Paper, PenLineIcon, PencilIcon, PhoneIcon, PinIcon, PlusIcon, Popover, RefreshCwIcon, SearchBar, Select, SelectItem, Separator, SettingsIcon, Skeleton, StickyNoteIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableRow, Tabs, TagIcon, TextField, ToggleButtonGroup, Tooltip, Trash2Icon, TrendingUpIcon, TypeIcon, Typography, UserIcon, UserPlus, UsersIcon, VideoIcon, Wand2Icon, WrenchIcon, XIcon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize } from "@rebasepro/ui";
|
|
3
3
|
import { ALL_WHERE_FILTER_OPS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, RebaseApiError, Vector, getDataSourceCapabilities, isLazyComponentRef, isRelationalCollectionConfig } from "@rebasepro/types";
|
|
4
4
|
import { UNRENDERED_SLOTS, resolveAdminCollection } from "@rebasepro/admin-types";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1007,7 +1007,7 @@ var DEFAULT_PAGE_SIZE$1 = 10;
|
|
|
1007
1007
|
/**
|
|
1008
1008
|
* Hook to manage relation selection with data fetching from Rebase data source
|
|
1009
1009
|
*/
|
|
1010
|
-
function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty }) {
|
|
1010
|
+
function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty, enabled = true }) {
|
|
1011
1011
|
const dataClient = useData();
|
|
1012
1012
|
const [items, setItems] = useState([]);
|
|
1013
1013
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -1018,6 +1018,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
|
|
|
1018
1018
|
const [limit, setLimit] = useState(pageSize);
|
|
1019
1019
|
const unsubscribeRef = useRef(null);
|
|
1020
1020
|
const searchTimeoutRef = useRef(null);
|
|
1021
|
+
const hasLoadedRef = useRef(false);
|
|
1021
1022
|
const setLoading = useCallback((loading) => {
|
|
1022
1023
|
isLoadingRef.current = loading;
|
|
1023
1024
|
setIsLoading(loading);
|
|
@@ -1066,12 +1067,14 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
|
|
|
1066
1067
|
const whereParams = fixedFilter && Object.keys(fixedFilter).length > 0 ? fixedFilter : void 0;
|
|
1067
1068
|
const onEntitiesUpdate = (res) => {
|
|
1068
1069
|
const newItems = res.data.map((e) => entityToRelationItem(e));
|
|
1070
|
+
hasLoadedRef.current = true;
|
|
1069
1071
|
setItems(newItems);
|
|
1070
1072
|
setHasMore(res.meta.hasMore);
|
|
1071
1073
|
setLoading(false);
|
|
1072
1074
|
};
|
|
1073
1075
|
const onErrorUpdate = (fetchError) => {
|
|
1074
1076
|
console.error("useRelationSelector: Error fetching data:", fetchError);
|
|
1077
|
+
hasLoadedRef.current = true;
|
|
1075
1078
|
setError(fetchError);
|
|
1076
1079
|
setLoading(false);
|
|
1077
1080
|
};
|
|
@@ -1141,11 +1144,16 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
|
|
|
1141
1144
|
setLoading
|
|
1142
1145
|
]);
|
|
1143
1146
|
useEffect(() => {
|
|
1147
|
+
if (!enabled) return;
|
|
1144
1148
|
fetchData();
|
|
1145
1149
|
return () => {
|
|
1146
1150
|
cleanupSubscription();
|
|
1147
1151
|
};
|
|
1148
|
-
}, [
|
|
1152
|
+
}, [
|
|
1153
|
+
fetchData,
|
|
1154
|
+
enabled,
|
|
1155
|
+
cleanupSubscription
|
|
1156
|
+
]);
|
|
1149
1157
|
useEffect(() => {
|
|
1150
1158
|
return () => {
|
|
1151
1159
|
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
|
@@ -1153,7 +1161,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
|
|
|
1153
1161
|
}, []);
|
|
1154
1162
|
return useMemo(() => ({
|
|
1155
1163
|
items,
|
|
1156
|
-
isLoading,
|
|
1164
|
+
isLoading: isLoading || enabled && !hasLoadedRef.current,
|
|
1157
1165
|
error,
|
|
1158
1166
|
search,
|
|
1159
1167
|
loadMore,
|
|
@@ -1166,7 +1174,8 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
|
|
|
1166
1174
|
search,
|
|
1167
1175
|
loadMore,
|
|
1168
1176
|
hasMore,
|
|
1169
|
-
entityToRelationItem
|
|
1177
|
+
entityToRelationItem,
|
|
1178
|
+
enabled
|
|
1170
1179
|
]);
|
|
1171
1180
|
}
|
|
1172
1181
|
//#endregion
|
|
@@ -9358,6 +9367,133 @@ function UserDisplay({ user }) {
|
|
|
9358
9367
|
});
|
|
9359
9368
|
}
|
|
9360
9369
|
//#endregion
|
|
9370
|
+
//#region src/components/LoginView/oauth-redirect-flow.ts
|
|
9371
|
+
/**
|
|
9372
|
+
* The browser half of the OAuth authorization-code flow.
|
|
9373
|
+
*
|
|
9374
|
+
* The server only ever sees a `code` and a `redirectUri` taken from the
|
|
9375
|
+
* request body, so `state`, PKCE and the binding between "this browser started
|
|
9376
|
+
* a login" and "this code came back" are, by construction, the client's job.
|
|
9377
|
+
* In the admin login they had been nobody's job: the authorize URL carried no
|
|
9378
|
+
* `state`, and the return leg accepted any `?code=` as long as a localStorage
|
|
9379
|
+
* marker naming the *provider* was set — a marker written on button click and
|
|
9380
|
+
* removed only on a callback that carried a code, so abandoning the flow left
|
|
9381
|
+
* it set indefinitely.
|
|
9382
|
+
*
|
|
9383
|
+
* Everything here is provider-agnostic on purpose. A button that wants a new
|
|
9384
|
+
* provider calls {@link startOAuthRedirect} and gets state, expiry and PKCE
|
|
9385
|
+
* without deciding anything, which is what stops the next provider shipping
|
|
9386
|
+
* without them.
|
|
9387
|
+
*/
|
|
9388
|
+
/** sessionStorage, not localStorage: a login attempt belongs to one tab. */
|
|
9389
|
+
var STORAGE_KEY = "rebase_oauth_redirect";
|
|
9390
|
+
/** How long a started authorization stays valid. */
|
|
9391
|
+
var PENDING_TTL_MS = 600 * 1e3;
|
|
9392
|
+
function defaultStorage() {
|
|
9393
|
+
try {
|
|
9394
|
+
return window.sessionStorage;
|
|
9395
|
+
} catch {
|
|
9396
|
+
return null;
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
function base64Url(bytes) {
|
|
9400
|
+
let binary = "";
|
|
9401
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
9402
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9403
|
+
}
|
|
9404
|
+
function randomToken(byteLength = 32) {
|
|
9405
|
+
const bytes = new Uint8Array(byteLength);
|
|
9406
|
+
crypto.getRandomValues(bytes);
|
|
9407
|
+
return base64Url(bytes);
|
|
9408
|
+
}
|
|
9409
|
+
async function pkceChallenge(verifier) {
|
|
9410
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
9411
|
+
return base64Url(new Uint8Array(digest));
|
|
9412
|
+
}
|
|
9413
|
+
function readPendingOAuthRedirect(storage = defaultStorage()) {
|
|
9414
|
+
if (!storage) return null;
|
|
9415
|
+
const raw = storage.getItem(STORAGE_KEY);
|
|
9416
|
+
if (!raw) return null;
|
|
9417
|
+
try {
|
|
9418
|
+
const parsed = JSON.parse(raw);
|
|
9419
|
+
if (!parsed?.provider || !parsed?.state) return null;
|
|
9420
|
+
return parsed;
|
|
9421
|
+
} catch {
|
|
9422
|
+
return null;
|
|
9423
|
+
}
|
|
9424
|
+
}
|
|
9425
|
+
function clearPendingOAuthRedirect(storage = defaultStorage()) {
|
|
9426
|
+
storage?.removeItem(STORAGE_KEY);
|
|
9427
|
+
}
|
|
9428
|
+
/**
|
|
9429
|
+
* Build the authorize URL and the pending record that will validate its
|
|
9430
|
+
* callback. Split out from {@link startOAuthRedirect} so it can be tested
|
|
9431
|
+
* without navigating.
|
|
9432
|
+
*/
|
|
9433
|
+
async function buildOAuthAuthorization(request, now = Date.now()) {
|
|
9434
|
+
const state = randomToken();
|
|
9435
|
+
const url = new URL(request.authorizeUrl);
|
|
9436
|
+
url.searchParams.set("response_type", "code");
|
|
9437
|
+
url.searchParams.set("client_id", request.clientId);
|
|
9438
|
+
url.searchParams.set("redirect_uri", request.redirectUri);
|
|
9439
|
+
url.searchParams.set("scope", request.scope);
|
|
9440
|
+
url.searchParams.set("state", state);
|
|
9441
|
+
for (const [key, value] of Object.entries(request.params ?? {})) url.searchParams.set(key, value);
|
|
9442
|
+
let codeVerifier;
|
|
9443
|
+
if (request.pkce) {
|
|
9444
|
+
codeVerifier = randomToken(32);
|
|
9445
|
+
url.searchParams.set("code_challenge", await pkceChallenge(codeVerifier));
|
|
9446
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
9447
|
+
}
|
|
9448
|
+
return {
|
|
9449
|
+
url: url.toString(),
|
|
9450
|
+
pending: {
|
|
9451
|
+
provider: request.provider,
|
|
9452
|
+
state,
|
|
9453
|
+
codeVerifier,
|
|
9454
|
+
redirectUri: request.redirectUri,
|
|
9455
|
+
startedAt: now
|
|
9456
|
+
}
|
|
9457
|
+
};
|
|
9458
|
+
}
|
|
9459
|
+
/** Remember the authorization and send the browser to the provider. */
|
|
9460
|
+
async function startOAuthRedirect(request, storage = defaultStorage()) {
|
|
9461
|
+
const { url, pending } = await buildOAuthAuthorization(request);
|
|
9462
|
+
storage?.setItem(STORAGE_KEY, JSON.stringify(pending));
|
|
9463
|
+
window.location.href = url;
|
|
9464
|
+
}
|
|
9465
|
+
/**
|
|
9466
|
+
* Interpret a return from the provider.
|
|
9467
|
+
*
|
|
9468
|
+
* Clears the pending record on **every** path that saw a callback — including
|
|
9469
|
+
* `?error=` and a state mismatch — so a declined or abandoned consent screen
|
|
9470
|
+
* cannot leave the browser primed to accept somebody else's code later.
|
|
9471
|
+
*/
|
|
9472
|
+
function consumeOAuthCallback(search, storage = defaultStorage(), now = Date.now()) {
|
|
9473
|
+
const params = new URLSearchParams(search);
|
|
9474
|
+
const code = params.get("code");
|
|
9475
|
+
const state = params.get("state");
|
|
9476
|
+
const error = params.get("error");
|
|
9477
|
+
if (!code && !error && !state) return { status: "none" };
|
|
9478
|
+
const pending = readPendingOAuthRedirect(storage);
|
|
9479
|
+
clearPendingOAuthRedirect(storage);
|
|
9480
|
+
if (error) return {
|
|
9481
|
+
status: "error",
|
|
9482
|
+
error
|
|
9483
|
+
};
|
|
9484
|
+
if (!code) return { status: "mismatch" };
|
|
9485
|
+
if (!pending) return { status: "mismatch" };
|
|
9486
|
+
if (now - pending.startedAt > PENDING_TTL_MS) return { status: "mismatch" };
|
|
9487
|
+
if (!state || state !== pending.state) return { status: "mismatch" };
|
|
9488
|
+
return {
|
|
9489
|
+
status: "ok",
|
|
9490
|
+
provider: pending.provider,
|
|
9491
|
+
code,
|
|
9492
|
+
codeVerifier: pending.codeVerifier,
|
|
9493
|
+
redirectUri: pending.redirectUri
|
|
9494
|
+
};
|
|
9495
|
+
}
|
|
9496
|
+
//#endregion
|
|
9361
9497
|
//#region src/components/LoginView/LoginView.tsx
|
|
9362
9498
|
/**
|
|
9363
9499
|
* The shared field background mixin (`dark:bg-black/30`) is invisible on the
|
|
@@ -9427,19 +9563,25 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
|
|
|
9427
9563
|
return () => clearTimeout(timer);
|
|
9428
9564
|
}, []);
|
|
9429
9565
|
useEffect(() => {
|
|
9430
|
-
const
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
});
|
|
9566
|
+
const result = consumeOAuthCallback(window.location.search);
|
|
9567
|
+
if (result.status === "none") return;
|
|
9568
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
9569
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
9570
|
+
if (result.status === "error") {
|
|
9571
|
+
console.error(`OAuth sign-in failed: ${result.error}`);
|
|
9572
|
+
return;
|
|
9573
|
+
}
|
|
9574
|
+
if (result.status === "mismatch") {
|
|
9575
|
+
console.error("Ignoring an OAuth code that does not match a sign-in this browser started.");
|
|
9576
|
+
return;
|
|
9442
9577
|
}
|
|
9578
|
+
if (authController.oauthLogin) authController.oauthLogin(result.provider, {
|
|
9579
|
+
code: result.code,
|
|
9580
|
+
redirectUri: result.redirectUri,
|
|
9581
|
+
...result.codeVerifier ? { codeVerifier: result.codeVerifier } : {}
|
|
9582
|
+
}).catch((err) => {
|
|
9583
|
+
console.error(`${result.provider} login failed:`, err);
|
|
9584
|
+
});
|
|
9443
9585
|
}, [authController]);
|
|
9444
9586
|
let logoComponent;
|
|
9445
9587
|
if (logo) logoComponent = /* @__PURE__ */ jsx("img", {
|
|
@@ -9710,10 +9852,13 @@ var GitHubIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
|
9710
9852
|
});
|
|
9711
9853
|
function GitHubLoginButton({ disabled, githubClientId }) {
|
|
9712
9854
|
const handleClick = () => {
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9855
|
+
startOAuthRedirect({
|
|
9856
|
+
provider: "github",
|
|
9857
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
9858
|
+
clientId: githubClientId,
|
|
9859
|
+
redirectUri: window.location.origin + window.location.pathname,
|
|
9860
|
+
scope: "read:user,user:email"
|
|
9861
|
+
});
|
|
9717
9862
|
};
|
|
9718
9863
|
return /* @__PURE__ */ jsx(LoginButton, {
|
|
9719
9864
|
disabled,
|
|
@@ -9731,10 +9876,13 @@ var LinkedInIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
|
9731
9876
|
});
|
|
9732
9877
|
function LinkedInLoginButton({ disabled, linkedinClientId }) {
|
|
9733
9878
|
const handleClick = () => {
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9879
|
+
startOAuthRedirect({
|
|
9880
|
+
provider: "linkedin",
|
|
9881
|
+
authorizeUrl: "https://www.linkedin.com/oauth/v2/authorization",
|
|
9882
|
+
clientId: linkedinClientId,
|
|
9883
|
+
redirectUri: window.location.origin + window.location.pathname,
|
|
9884
|
+
scope: "openid profile email"
|
|
9885
|
+
});
|
|
9738
9886
|
};
|
|
9739
9887
|
return /* @__PURE__ */ jsx(LoginButton, {
|
|
9740
9888
|
disabled,
|
|
@@ -9746,6 +9894,9 @@ function LinkedInLoginButton({ disabled, linkedinClientId }) {
|
|
|
9746
9894
|
function LoginForm({ onClose, onForgotPassword, authController, registrationMode, noUserComponent, disableSignupScreen, bootstrapMode = false, switchToRegister, switchToLogin, defaultEmail, defaultPassword, onNewsletterOptIn, newsletterOptIn = false, setNewsletterOptIn }) {
|
|
9747
9895
|
const passwordRef = useRef(null);
|
|
9748
9896
|
const { t } = useTranslation();
|
|
9897
|
+
const emailId = useId();
|
|
9898
|
+
const passwordId = useId();
|
|
9899
|
+
const displayNameId = useId();
|
|
9749
9900
|
const [email, setEmail] = useState(defaultEmail);
|
|
9750
9901
|
const [password, setPassword] = useState(defaultPassword);
|
|
9751
9902
|
const [displayName, setDisplayName] = useState();
|
|
@@ -9814,11 +9965,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
|
|
|
9814
9965
|
className: "w-full mb-3",
|
|
9815
9966
|
children: [/* @__PURE__ */ jsx(Typography, {
|
|
9816
9967
|
variant: "label",
|
|
9968
|
+
component: "label",
|
|
9817
9969
|
color: "secondary",
|
|
9818
9970
|
className: "mb-1",
|
|
9971
|
+
htmlFor: displayNameId,
|
|
9819
9972
|
children: "Display Name"
|
|
9820
9973
|
}), /* @__PURE__ */ jsx(TextField, {
|
|
9821
9974
|
placeholder: "Jane Doe (optional)",
|
|
9975
|
+
id: displayNameId,
|
|
9822
9976
|
className: loginFieldClasses,
|
|
9823
9977
|
value: displayName ?? "",
|
|
9824
9978
|
disabled: authController.initialLoading,
|
|
@@ -9831,11 +9985,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
|
|
|
9831
9985
|
className: "w-full mb-3",
|
|
9832
9986
|
children: [/* @__PURE__ */ jsx(Typography, {
|
|
9833
9987
|
variant: "label",
|
|
9988
|
+
component: "label",
|
|
9834
9989
|
color: "secondary",
|
|
9835
9990
|
className: "mb-1",
|
|
9991
|
+
htmlFor: emailId,
|
|
9836
9992
|
children: "Email"
|
|
9837
9993
|
}), /* @__PURE__ */ jsx(TextField, {
|
|
9838
9994
|
placeholder: "you@example.com",
|
|
9995
|
+
id: emailId,
|
|
9839
9996
|
className: loginFieldClasses,
|
|
9840
9997
|
autoFocus: true,
|
|
9841
9998
|
value: email ?? "",
|
|
@@ -9849,11 +10006,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
|
|
|
9849
10006
|
className: "w-full mb-1",
|
|
9850
10007
|
children: [/* @__PURE__ */ jsx(Typography, {
|
|
9851
10008
|
variant: "label",
|
|
10009
|
+
component: "label",
|
|
9852
10010
|
color: "secondary",
|
|
9853
10011
|
className: "mb-1",
|
|
10012
|
+
htmlFor: passwordId,
|
|
9854
10013
|
children: "Password"
|
|
9855
10014
|
}), /* @__PURE__ */ jsx(TextField, {
|
|
9856
10015
|
placeholder: "••••••••",
|
|
10016
|
+
id: passwordId,
|
|
9857
10017
|
className: loginFieldClasses,
|
|
9858
10018
|
value: password ?? "",
|
|
9859
10019
|
disabled: authController.initialLoading,
|
|
@@ -9942,6 +10102,7 @@ function ForgotPasswordForm({ onClose, authController }) {
|
|
|
9942
10102
|
const [email, setEmail] = useState("");
|
|
9943
10103
|
const [submitted, setSubmitted] = useState(false);
|
|
9944
10104
|
const [error, setError] = useState(null);
|
|
10105
|
+
const emailId = useId();
|
|
9945
10106
|
useEffect(() => {
|
|
9946
10107
|
if (!document) return;
|
|
9947
10108
|
const escFunction = (event) => {
|
|
@@ -10042,11 +10203,14 @@ function ForgotPasswordForm({ onClose, authController }) {
|
|
|
10042
10203
|
className: "w-full mb-3",
|
|
10043
10204
|
children: [/* @__PURE__ */ jsx(Typography, {
|
|
10044
10205
|
variant: "label",
|
|
10206
|
+
component: "label",
|
|
10045
10207
|
color: "secondary",
|
|
10046
10208
|
className: "mb-1",
|
|
10209
|
+
htmlFor: emailId,
|
|
10047
10210
|
children: "Email"
|
|
10048
10211
|
}), /* @__PURE__ */ jsx(TextField, {
|
|
10049
10212
|
placeholder: "you@example.com",
|
|
10213
|
+
id: emailId,
|
|
10050
10214
|
className: loginFieldClasses,
|
|
10051
10215
|
autoFocus: true,
|
|
10052
10216
|
value: email,
|
|
@@ -10470,6 +10634,7 @@ var en = {
|
|
|
10470
10634
|
flatten_arrays: "Flatten arrays",
|
|
10471
10635
|
download: "Download",
|
|
10472
10636
|
large_number_of_documents: "This collection has a large number of documents ({{count}}).",
|
|
10637
|
+
too_many_documents_to_export: "This collection has more documents ({{count}}) than the browser can export in a single file (limit {{limit}}). Filter it down before exporting.",
|
|
10473
10638
|
include_undefined_values: "Include undefined values",
|
|
10474
10639
|
submit: "Submit",
|
|
10475
10640
|
no_filterable_properties: "No filterable properties available",
|
|
@@ -11368,6 +11533,7 @@ var es = {
|
|
|
11368
11533
|
flatten_arrays: "Aplanar arrays",
|
|
11369
11534
|
download: "Descargar",
|
|
11370
11535
|
large_number_of_documents: "Esta colección posee un gran número de documentos ({{count}}).",
|
|
11536
|
+
too_many_documents_to_export: "Esta colección tiene más documentos ({{count}}) de los que el navegador puede exportar en un solo archivo (límite {{limit}}). Fíltrala antes de exportar.",
|
|
11371
11537
|
include_undefined_values: "Incluir valores omitidos (undefined)",
|
|
11372
11538
|
submit: "Enviar",
|
|
11373
11539
|
no_filterable_properties: "No hay propiedades filtrables disponibles",
|
|
@@ -12225,6 +12391,7 @@ var de = {
|
|
|
12225
12391
|
flatten_arrays: "Arrays glätten",
|
|
12226
12392
|
download: "Herunterladen",
|
|
12227
12393
|
large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
|
|
12394
|
+
too_many_documents_to_export: "Diese Sammlung enthält mehr Dokumente ({{count}}), als der Browser in einer einzigen Datei exportieren kann (Grenze {{limit}}). Filtern Sie sie vor dem Export.",
|
|
12228
12395
|
include_undefined_values: "Undefinierte Werte einschließen",
|
|
12229
12396
|
submit: "Einreichen",
|
|
12230
12397
|
no_filterable_properties: "Keine filterbaren Eigenschaften verfügbar",
|
|
@@ -13072,6 +13239,7 @@ var fr = {
|
|
|
13072
13239
|
flatten_arrays: "Aplatir les tableaux",
|
|
13073
13240
|
download: "Télécharger",
|
|
13074
13241
|
large_number_of_documents: "Cette collection contient un grand nombre de documents ({{count}}).",
|
|
13242
|
+
too_many_documents_to_export: "Cette collection contient plus de documents ({{count}}) que le navigateur ne peut en exporter dans un seul fichier (limite {{limit}}). Filtrez-la avant d'exporter.",
|
|
13075
13243
|
include_undefined_values: "Inclure les valeurs non définies",
|
|
13076
13244
|
submit: "Soumettre",
|
|
13077
13245
|
no_filterable_properties: "Aucune propriété filtrable disponible",
|
|
@@ -13919,6 +14087,7 @@ var it = {
|
|
|
13919
14087
|
flatten_arrays: "Appiattisci gli array",
|
|
13920
14088
|
download: "Scarica",
|
|
13921
14089
|
large_number_of_documents: "Questa collezione contiene un numero elevato di documenti ({{count}})",
|
|
14090
|
+
too_many_documents_to_export: "Questa collezione contiene più documenti ({{count}}) di quanti il browser possa esportarne in un solo file (limite {{limit}}). Filtrala prima di esportare.",
|
|
13922
14091
|
include_undefined_values: "Includi valori non definiti",
|
|
13923
14092
|
submit: "Invia",
|
|
13924
14093
|
no_filterable_properties: "Nessuna proprietà filtrabile disponibile",
|
|
@@ -14766,6 +14935,7 @@ var hi = {
|
|
|
14766
14935
|
flatten_arrays: "ऐरे को फ़्लैट करें",
|
|
14767
14936
|
download: "डाउनलोड",
|
|
14768
14937
|
large_number_of_documents: "इस संग्रह में बड़ी संख्या में दस्तावेज़ हैं ({{count}})।",
|
|
14938
|
+
too_many_documents_to_export: "इस संग्रह में इतने दस्तावेज़ ({{count}}) हैं कि ब्राउज़र उन्हें एक ही फ़ाइल में निर्यात नहीं कर सकता (सीमा {{limit}})। निर्यात से पहले इसे फ़िल्टर करें।",
|
|
14769
14939
|
include_undefined_values: "अपरिभाषित (undefined) मान शामिल करें",
|
|
14770
14940
|
submit: "सबमिट करें",
|
|
14771
14941
|
no_filterable_properties: "कोई फ़िल्टर करने योग्य गुण उपलब्ध नहीं हैं",
|
|
@@ -15618,6 +15788,7 @@ var pt = {
|
|
|
15618
15788
|
flatten_arrays: "Achatar listas",
|
|
15619
15789
|
download: "Descarregar",
|
|
15620
15790
|
large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
|
|
15791
|
+
too_many_documents_to_export: "Esta coleção tem mais documentos ({{count}}) do que o navegador consegue exportar num único ficheiro (limite {{limit}}). Filtre-a antes de exportar.",
|
|
15621
15792
|
include_undefined_values: "Incluir valores indefinidos",
|
|
15622
15793
|
submit: "Submeter",
|
|
15623
15794
|
no_filterable_properties: "Não há propriedades filtráveis disponíveis",
|
|
@@ -16548,6 +16719,7 @@ function useRebaseAuthController(props = {}) {
|
|
|
16548
16719
|
const syncState = async (event, session) => {
|
|
16549
16720
|
await updateState(session);
|
|
16550
16721
|
if (event === "SIGNED_OUT") {
|
|
16722
|
+
clearFetchCache();
|
|
16551
16723
|
if (isMountedRef.current) setLoginSkipped(false);
|
|
16552
16724
|
onSignOutRef.current?.();
|
|
16553
16725
|
}
|
|
@@ -16876,23 +17048,13 @@ var iconKeysMap = iconKeys.reduce((acc, key) => {
|
|
|
16876
17048
|
acc[key] = key;
|
|
16877
17049
|
return acc;
|
|
16878
17050
|
}, {});
|
|
16879
|
-
function toPascalCase(str) {
|
|
16880
|
-
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
16881
|
-
}
|
|
16882
|
-
/**
|
|
16883
|
-
* Resolve a Lucide icon component by string key.
|
|
16884
|
-
* Tries direct match, PascalCase conversion, and falls back to CircleAlert.
|
|
16885
|
-
*/
|
|
16886
|
-
function resolveIcon(iconKey) {
|
|
16887
|
-
const iconsMap = lucideIcons;
|
|
16888
|
-
let icon = iconsMap[iconKey];
|
|
16889
|
-
if (!icon) icon = iconsMap[toPascalCase(iconKey)];
|
|
16890
|
-
if (!icon) icon = iconsMap.CircleAlert;
|
|
16891
|
-
return icon ?? null;
|
|
16892
|
-
}
|
|
16893
17051
|
/**
|
|
16894
17052
|
* Render an icon element from a string key or existing React element.
|
|
16895
|
-
*
|
|
17053
|
+
*
|
|
17054
|
+
* Whether a key names an icon is decided here, against `iconKeys` — an array
|
|
17055
|
+
* of strings. The component behind the key is fetched by `LucideIconByName` on
|
|
17056
|
+
* first use: this used to index lucide's full `icons` map, which is the whole
|
|
17057
|
+
* library and cannot be tree-shaken.
|
|
16896
17058
|
*/
|
|
16897
17059
|
function getIcon(iconKey, className, color, size) {
|
|
16898
17060
|
if (React.isValidElement(iconKey)) return iconKey;
|
|
@@ -16902,9 +17064,8 @@ function getIcon(iconKey, className, color, size) {
|
|
|
16902
17064
|
const slugifiedKey = slugify(iconKey).replace(/-/g, "_");
|
|
16903
17065
|
const mappedKey = iconKeysMap[iconKey] || iconKeysMap[lowerKey] || iconKeysMap[slugifiedKey];
|
|
16904
17066
|
if (!mappedKey) return;
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
return /* @__PURE__ */ jsx(LucideIcon, {
|
|
17067
|
+
return /* @__PURE__ */ jsx(LucideIconByName, {
|
|
17068
|
+
name: mappedKey,
|
|
16908
17069
|
size: typeof size === "number" ? size : iconSize[size ?? "medium"],
|
|
16909
17070
|
className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
|
|
16910
17071
|
});
|
|
@@ -16924,10 +17085,10 @@ var IconForView = React.memo(function IconForView({ collectionOrView, className,
|
|
|
16924
17085
|
}
|
|
16925
17086
|
const iconsCount = coolIconKeys.length;
|
|
16926
17087
|
if (!key) key = coolIconKeys[hashString(collectionOrView.slug) % iconsCount];
|
|
16927
|
-
const
|
|
16928
|
-
|
|
16929
|
-
|
|
16930
|
-
size:
|
|
17088
|
+
const sizeInPx = typeof size === "number" ? size : iconSize[size];
|
|
17089
|
+
return /* @__PURE__ */ jsx(LucideIconByName, {
|
|
17090
|
+
name: key,
|
|
17091
|
+
size: sizeInPx,
|
|
16931
17092
|
className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
|
|
16932
17093
|
});
|
|
16933
17094
|
}, (prevProps, nextProps) => {
|