@rebasepro/firebase 0.13.0 → 0.13.1-canary.g06dbe5b
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/RebaseFirebaseAppProps.d.ts +1 -1
- package/dist/hooks/useBuildUserManagement.d.ts +29 -0
- package/dist/hooks/useFirebaseAuthController.d.ts +14 -0
- package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +39 -1
- package/dist/hooks/useFirestoreDriver.d.ts +20 -0
- package/dist/index.es.js +168 -28
- package/dist/index.es.js.map +1 -1
- package/package.json +8 -8
- package/src/components/RebaseFirebaseAppProps.tsx +1 -1
- package/src/hooks/useBuildUserManagement.tsx +66 -5
- package/src/hooks/useFirebaseAuthController.ts +28 -3
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +183 -29
- package/src/hooks/useFirestoreDriver.ts +40 -3
|
@@ -35,6 +35,35 @@ export interface UserManagementDelegateParams<CONTROLLER extends AuthController<
|
|
|
35
35
|
*/
|
|
36
36
|
allowDefaultRolesCreation?: boolean;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Why the access gate answered the way it did.
|
|
40
|
+
*
|
|
41
|
+
* `users-unreadable` is a state of its own because it used to be
|
|
42
|
+
* indistinguishable from `bootstrap`: the users listener's `onError` empties
|
|
43
|
+
* the user list, so a `permission-denied` on the users path — a rules
|
|
44
|
+
* misconfiguration, a rules deploy that has not landed, a renamed path —
|
|
45
|
+
* reached the gate as "no users created yet" and every authenticated user was
|
|
46
|
+
* let in. An unreadable list is not an empty one, and only one of the two may
|
|
47
|
+
* open the door.
|
|
48
|
+
*/
|
|
49
|
+
export type AccessDecision = "loading" | "no-user" | "users-unreadable" | "bootstrap" | "known-user" | "unknown-user";
|
|
50
|
+
/**
|
|
51
|
+
* Decide whether a user may access the CMS, given the state of the user
|
|
52
|
+
* management collection.
|
|
53
|
+
*
|
|
54
|
+
* A plain function rather than logic inside the gate callback so the states
|
|
55
|
+
* that must stay distinct can be asserted without a React render.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveAccessDecision({ loading, usersError, users, user }: {
|
|
58
|
+
loading: boolean;
|
|
59
|
+
usersError?: Error;
|
|
60
|
+
users: {
|
|
61
|
+
email?: string | null;
|
|
62
|
+
}[];
|
|
63
|
+
user: {
|
|
64
|
+
email?: string | null;
|
|
65
|
+
} | null;
|
|
66
|
+
}): AccessDecision;
|
|
38
67
|
/**
|
|
39
68
|
* This hook is used to build a user management object that can be used to
|
|
40
69
|
* manage users and roles in a Firestore backend.
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { FirebaseApp } from "firebase/app";
|
|
2
2
|
import { FirebaseAuthController, FirebaseSignInOption, FirebaseSignInProvider, FirebaseUserWrapper } from "../types";
|
|
3
3
|
import type { User } from "@rebasepro/types";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve `user`'s roles through `defineRolesFor` and report whether they
|
|
6
|
+
* differ from the ones already applied.
|
|
7
|
+
*
|
|
8
|
+
* A plain function rather than a check inside the hook because the check that
|
|
9
|
+
* used to live there read `!equal(userRoles, userRoles)` — the local shadowed
|
|
10
|
+
* the state of the same name, so the fresh roles were compared to themselves,
|
|
11
|
+
* the guard was never true, and a `defineRolesFor` result arriving after the
|
|
12
|
+
* auth-state change never reached the controller.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveRoleRefresh(defineRolesFor: (user: User) => Promise<string[] | undefined> | string[] | undefined, user: User, currentRoles: string[] | undefined): Promise<{
|
|
15
|
+
changed: boolean;
|
|
16
|
+
roles: string[] | undefined;
|
|
17
|
+
}>;
|
|
4
18
|
export interface FirebaseAuthControllerProps {
|
|
5
19
|
loading?: boolean;
|
|
6
20
|
firebaseApp?: FirebaseApp;
|
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
import { FirebaseApp } from "firebase/app";
|
|
2
|
-
import { DataDriver } from "@rebasepro/types";
|
|
2
|
+
import { DataDriver, FetchCollectionProps } from "@rebasepro/types";
|
|
3
|
+
/** The values the Realtime Database can order or bound a query by. */
|
|
4
|
+
type RTDBFilterValue = string | number | boolean | null;
|
|
5
|
+
/**
|
|
6
|
+
* A read expressed in the Realtime Database's own query model.
|
|
7
|
+
*
|
|
8
|
+
* @see planRTDBQuery
|
|
9
|
+
*/
|
|
10
|
+
export type RTDBQueryPlan = {
|
|
11
|
+
/** Child key to order — and therefore to bound — by. Absent means order by key. */
|
|
12
|
+
orderByChild?: string;
|
|
13
|
+
equalTo?: RTDBFilterValue;
|
|
14
|
+
startAt?: RTDBFilterValue;
|
|
15
|
+
/** Key the window starts after, exclusive. Only valid in key order. */
|
|
16
|
+
startAfter?: RTDBFilterValue;
|
|
17
|
+
endAt?: RTDBFilterValue;
|
|
18
|
+
limitToFirst?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Rows to drop from the front of the result — the caller's `offset`, which
|
|
21
|
+
* the database has no constraint for. Applied by the caller, not by
|
|
22
|
+
* {@link rtdbConstraints}.
|
|
23
|
+
*/
|
|
24
|
+
skip?: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Translate a driver read into the Realtime Database's query model, or refuse it.
|
|
28
|
+
*
|
|
29
|
+
* The Realtime Database orders by a single child key per query and bounds that
|
|
30
|
+
* one key with `equalTo`/`startAt`/`endAt`. Nothing else is expressible: no
|
|
31
|
+
* second field, no descending order, no text search, no `or(...)` group.
|
|
32
|
+
*
|
|
33
|
+
* Everything beyond `limit` and `startAfter` used to be destructured out of the
|
|
34
|
+
* read and then never referenced, so a caller asking for `status == "draft"`
|
|
35
|
+
* was handed the entire collection, presented as the answer. A query this
|
|
36
|
+
* database cannot express is refused here instead — a caller that sees an error
|
|
37
|
+
* can fall back, a caller that sees the wrong rows cannot.
|
|
38
|
+
*/
|
|
39
|
+
export declare function planRTDBQuery<M extends Record<string, any>>({ filter, orderBy, order, searchString, logical, limit, offset, startAfter: startAfterKey }: Pick<FetchCollectionProps<M>, "filter" | "orderBy" | "order" | "searchString" | "logical" | "limit" | "offset" | "startAfter">): RTDBQueryPlan;
|
|
3
40
|
export declare function useFirebaseRTDBDelegate({ firebaseApp }: {
|
|
4
41
|
firebaseApp?: FirebaseApp;
|
|
5
42
|
}): DataDriver;
|
|
43
|
+
export {};
|
|
@@ -36,6 +36,26 @@ export type FirestoreDataDriver = DataDriver & {
|
|
|
36
36
|
collection?: CollectionConfig;
|
|
37
37
|
}) => Promise<boolean>;
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The window a read has to ask Firestore for in order to honour `offset`.
|
|
41
|
+
*
|
|
42
|
+
* Firestore's web SDK has no `offset()` — it pages by cursor (`startAfter`)
|
|
43
|
+
* only. Callers that page by offset (`buildRebaseData`, and through it every
|
|
44
|
+
* `findAll()` and `iterate()`) were therefore served page one every time:
|
|
45
|
+
* `count` is a real server count, so `hasMore` never went false, the walk
|
|
46
|
+
* accumulated the same rows over and over, and it ended by tripping its row
|
|
47
|
+
* cap and reporting "matched more than N rows" — a condition that had not
|
|
48
|
+
* occurred.
|
|
49
|
+
*
|
|
50
|
+
* So the read asks for `offset + limit` documents and drops the first
|
|
51
|
+
* `offset`. Those documents are billed either way: Firestore charges for every
|
|
52
|
+
* document a cursor walks past, which is why `startAfter` is the cheap way to
|
|
53
|
+
* page and offset paging over a large collection is not.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveOffsetWindow(limit: number | undefined, offset: number | undefined): {
|
|
56
|
+
fetchLimit: number | undefined;
|
|
57
|
+
skip: number;
|
|
58
|
+
};
|
|
39
59
|
/**
|
|
40
60
|
* Use this hook to build a {@link DataDriver} based on Firestore
|
|
41
61
|
* @param firebaseApp
|
package/dist/index.es.js
CHANGED
|
@@ -9,7 +9,7 @@ import { DocumentReference, GeoPoint as GeoPoint$1, Timestamp, VectorValue, coll
|
|
|
9
9
|
import { COLLECTION_PATH_SEPARATOR, buildRebaseData, sortProperties, stripCollectionPath } from "@rebasepro/common";
|
|
10
10
|
import Fuse from "fuse.js";
|
|
11
11
|
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
12
|
-
import { get, getDatabase, limitToFirst, onValue, orderByChild, orderByKey, push, query as query$1, ref as ref$1, remove, set, startAt } from "firebase/database";
|
|
12
|
+
import { endAt, equalTo, get, getDatabase, limitToFirst, onValue, orderByChild, orderByKey, push, query as query$1, ref as ref$1, remove, set, startAfter as startAfter$1, startAt } from "firebase/database";
|
|
13
13
|
import { removeUndefined } from "@rebasepro/utils";
|
|
14
14
|
import { AdminModeControllerProvider, ErrorView, ModeControllerProvider, Rebase, RebaseLogo, RebaseRoutes, SnackbarProvider, useBrowserTitleAndIcon, useBuildAdminModeController, useBuildLocalConfigurationPersistence, useBuildModeController, useModeController, useSnackbarController } from "@rebasepro/app";
|
|
15
15
|
import { AppBar, CollectionRegistryContext, Drawer, NavigationStateContext, RebaseRoute, Scaffold, SideDialogs, SidePanelProvider, UrlContext, useBuildCollectionRegistryController, useBuildNavigationStateController, useBuildUrlController } from "@rebasepro/admin";
|
|
@@ -18,6 +18,23 @@ import { Navigate, Outlet, Route } from "react-router";
|
|
|
18
18
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
19
|
//#region src/hooks/useFirebaseAuthController.ts
|
|
20
20
|
/**
|
|
21
|
+
* Resolve `user`'s roles through `defineRolesFor` and report whether they
|
|
22
|
+
* differ from the ones already applied.
|
|
23
|
+
*
|
|
24
|
+
* A plain function rather than a check inside the hook because the check that
|
|
25
|
+
* used to live there read `!equal(userRoles, userRoles)` — the local shadowed
|
|
26
|
+
* the state of the same name, so the fresh roles were compared to themselves,
|
|
27
|
+
* the guard was never true, and a `defineRolesFor` result arriving after the
|
|
28
|
+
* auth-state change never reached the controller.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveRoleRefresh(defineRolesFor, user, currentRoles) {
|
|
31
|
+
const roles = await defineRolesFor(user);
|
|
32
|
+
return {
|
|
33
|
+
changed: !deepEqual(currentRoles, roles),
|
|
34
|
+
roles
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
21
38
|
* Use this hook to build an {@link AuthController} based on Firebase Auth
|
|
22
39
|
* @group Firebase
|
|
23
40
|
*/
|
|
@@ -44,8 +61,8 @@ var useFirebaseAuthController = ({ loading, firebaseApp, signInOptions, onSignOu
|
|
|
44
61
|
}, [loading]);
|
|
45
62
|
const updateRoles = useCallback(async (user) => {
|
|
46
63
|
if (defineRolesFor && user) {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
64
|
+
const { changed, roles } = await resolveRoleRefresh(defineRolesFor, user, userRoles);
|
|
65
|
+
if (changed) setUserRoles(roles);
|
|
49
66
|
}
|
|
50
67
|
}, [defineRolesFor, userRoles]);
|
|
51
68
|
useEffect(() => {
|
|
@@ -944,6 +961,29 @@ function buildRebaseSearchController(options) {
|
|
|
944
961
|
//#endregion
|
|
945
962
|
//#region src/hooks/useFirestoreDriver.ts
|
|
946
963
|
/**
|
|
964
|
+
* The window a read has to ask Firestore for in order to honour `offset`.
|
|
965
|
+
*
|
|
966
|
+
* Firestore's web SDK has no `offset()` — it pages by cursor (`startAfter`)
|
|
967
|
+
* only. Callers that page by offset (`buildRebaseData`, and through it every
|
|
968
|
+
* `findAll()` and `iterate()`) were therefore served page one every time:
|
|
969
|
+
* `count` is a real server count, so `hasMore` never went false, the walk
|
|
970
|
+
* accumulated the same rows over and over, and it ended by tripping its row
|
|
971
|
+
* cap and reporting "matched more than N rows" — a condition that had not
|
|
972
|
+
* occurred.
|
|
973
|
+
*
|
|
974
|
+
* So the read asks for `offset + limit` documents and drops the first
|
|
975
|
+
* `offset`. Those documents are billed either way: Firestore charges for every
|
|
976
|
+
* document a cursor walks past, which is why `startAfter` is the cheap way to
|
|
977
|
+
* page and offset paging over a large collection is not.
|
|
978
|
+
*/
|
|
979
|
+
function resolveOffsetWindow(limit, offset) {
|
|
980
|
+
const skip = offset !== void 0 && Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0;
|
|
981
|
+
return {
|
|
982
|
+
fetchLimit: limit === void 0 ? void 0 : limit + skip,
|
|
983
|
+
skip
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
947
987
|
* Use this hook to build a {@link DataDriver} based on Firestore
|
|
948
988
|
* @param firebaseApp
|
|
949
989
|
* @param textSearchControllerBuilder
|
|
@@ -964,7 +1004,7 @@ function useFirestoreDriver({ firebaseApp, textSearchControllerBuilder, firestor
|
|
|
964
1004
|
localTextSearchEnabled,
|
|
965
1005
|
textSearchControllerBuilder
|
|
966
1006
|
]);
|
|
967
|
-
const buildQuery = useCallback((path, filter, orderBy$1, order, startAfter$
|
|
1007
|
+
const buildQuery = useCallback((path, filter, orderBy$1, order, startAfter$2, limit$1, databaseId) => {
|
|
968
1008
|
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
969
1009
|
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
970
1010
|
const collectionReference = collection(firestore, path);
|
|
@@ -983,7 +1023,7 @@ function useFirestoreDriver({ firebaseApp, textSearchControllerBuilder, firestor
|
|
|
983
1023
|
queryParams.push(where(key, op, rebaseToFirestoreModel(value, firestore)));
|
|
984
1024
|
});
|
|
985
1025
|
if (orderBy$1 && order) queryParams.push(orderBy(orderBy$1, order));
|
|
986
|
-
if (startAfter$
|
|
1026
|
+
if (startAfter$2) queryParams.push(startAfter(startAfter$2));
|
|
987
1027
|
if (limit$1) queryParams.push(limit(limit$1));
|
|
988
1028
|
return query(collectionReference, ...queryParams);
|
|
989
1029
|
}, [firebaseApp]);
|
|
@@ -1067,26 +1107,29 @@ function useFirestoreDriver({ firebaseApp, textSearchControllerBuilder, firestor
|
|
|
1067
1107
|
* @param collection
|
|
1068
1108
|
* @param filter
|
|
1069
1109
|
* @param limit
|
|
1110
|
+
* @param offset
|
|
1070
1111
|
* @param startAfter
|
|
1071
1112
|
* @param searchString
|
|
1072
1113
|
* @param orderBy
|
|
1073
1114
|
* @param order
|
|
1074
|
-
* @return
|
|
1115
|
+
* @return The rows in the requested window
|
|
1075
1116
|
* @see useCollection if you need this functionality implemented as a hook
|
|
1076
1117
|
* @group Firestore
|
|
1077
1118
|
*/
|
|
1078
|
-
const fetchCollection = useCallback(async ({ path, filter, limit, startAfter, searchString, orderBy, order, collection }) => {
|
|
1119
|
+
const fetchCollection = useCallback(async ({ path, filter, limit, offset, startAfter, searchString, orderBy, order, collection }) => {
|
|
1079
1120
|
const databaseId = collection?.databaseId;
|
|
1080
1121
|
const resolvedPath = path;
|
|
1081
1122
|
console.debug("Fetching collection", {
|
|
1082
1123
|
path,
|
|
1083
1124
|
limit,
|
|
1125
|
+
offset,
|
|
1084
1126
|
filter,
|
|
1085
1127
|
startAfter,
|
|
1086
1128
|
orderBy,
|
|
1087
1129
|
order
|
|
1088
1130
|
});
|
|
1089
|
-
|
|
1131
|
+
const { fetchLimit, skip } = resolveOffsetWindow(limit, offset);
|
|
1132
|
+
return (await getDocs(buildQuery(resolvedPath, filter, orderBy, order, startAfter, fetchLimit, databaseId))).docs.slice(skip).map((doc) => createRowFromDocument(doc));
|
|
1090
1133
|
}, [buildQuery]);
|
|
1091
1134
|
/**
|
|
1092
1135
|
* Listen to a entities in a given path
|
|
@@ -1384,30 +1427,99 @@ function buildTextSearchControllerWithLocalSearch({ textSearchControllerBuilder,
|
|
|
1384
1427
|
}
|
|
1385
1428
|
//#endregion
|
|
1386
1429
|
//#region src/hooks/useFirebaseRealTimeDBDelegate.ts
|
|
1430
|
+
var RTDB = "useFirebaseRTDBDelegate";
|
|
1431
|
+
var isRTDBValue = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1432
|
+
/**
|
|
1433
|
+
* Translate a driver read into the Realtime Database's query model, or refuse it.
|
|
1434
|
+
*
|
|
1435
|
+
* The Realtime Database orders by a single child key per query and bounds that
|
|
1436
|
+
* one key with `equalTo`/`startAt`/`endAt`. Nothing else is expressible: no
|
|
1437
|
+
* second field, no descending order, no text search, no `or(...)` group.
|
|
1438
|
+
*
|
|
1439
|
+
* Everything beyond `limit` and `startAfter` used to be destructured out of the
|
|
1440
|
+
* read and then never referenced, so a caller asking for `status == "draft"`
|
|
1441
|
+
* was handed the entire collection, presented as the answer. A query this
|
|
1442
|
+
* database cannot express is refused here instead — a caller that sees an error
|
|
1443
|
+
* can fall back, a caller that sees the wrong rows cannot.
|
|
1444
|
+
*/
|
|
1445
|
+
function planRTDBQuery({ filter, orderBy, order, searchString, logical, limit, offset, startAfter: startAfterKey }) {
|
|
1446
|
+
if (searchString) throw new Error(`${RTDB}: the Realtime Database has no text search, so \`searchString\` cannot be applied. Index the data in a search service instead.`);
|
|
1447
|
+
if (logical) throw new Error(`${RTDB}: the Realtime Database cannot evaluate \`or(...)\`/\`and(...)\` groups.`);
|
|
1448
|
+
if (order === "desc") throw new Error(`${RTDB}: the Realtime Database only orders ascending, so \`order: "desc"\` cannot be applied.`);
|
|
1449
|
+
const conditions = [];
|
|
1450
|
+
Object.entries(filter ?? {}).forEach(([key, entry]) => {
|
|
1451
|
+
if (!entry) return;
|
|
1452
|
+
(Array.isArray(entry[0]) ? entry : [entry]).forEach(([op, value]) => conditions.push([
|
|
1453
|
+
key,
|
|
1454
|
+
op,
|
|
1455
|
+
value
|
|
1456
|
+
]));
|
|
1457
|
+
});
|
|
1458
|
+
const fields = Array.from(new Set(conditions.map(([key]) => key)));
|
|
1459
|
+
if (fields.length > 1) throw new Error(`${RTDB}: the Realtime Database filters on one child key per query; this read asked for ${fields.join(", ")}.`);
|
|
1460
|
+
const [field] = fields;
|
|
1461
|
+
if (field && orderBy && orderBy !== field) throw new Error(`${RTDB}: a query is ordered by the key it filters on; cannot filter \`${field}\` while ordering by \`${orderBy}\`.`);
|
|
1462
|
+
const orderChild = field ?? orderBy;
|
|
1463
|
+
const plan = orderChild ? { orderByChild: orderChild } : {};
|
|
1464
|
+
for (const [key, op, value] of conditions) {
|
|
1465
|
+
if (!isRTDBValue(value)) throw new Error(`${RTDB}: cannot bound \`${key}\` by a ${Array.isArray(value) ? "array" : typeof value} value; the Realtime Database compares strings, numbers, booleans and null.`);
|
|
1466
|
+
if (op === "==") {
|
|
1467
|
+
if (conditions.length > 1) throw new Error(`${RTDB}: \`==\` bounds a query on its own; it cannot be combined with another condition on \`${key}\`.`);
|
|
1468
|
+
plan.equalTo = value;
|
|
1469
|
+
} else if (op === ">=") plan.startAt = value;
|
|
1470
|
+
else if (op === "<=") plan.endAt = value;
|
|
1471
|
+
else throw new Error(`${RTDB}: the Realtime Database does not support the "${op}" operator (on \`${key}\`). It bounds a single child key with ==, >= and <=.`);
|
|
1472
|
+
}
|
|
1473
|
+
if (startAfterKey !== void 0) {
|
|
1474
|
+
if (orderChild) throw new Error(`${RTDB}: \`startAfter\` pages in key order and cannot be combined with a filter or \`orderBy\`.`);
|
|
1475
|
+
plan.startAfter = String(startAfterKey);
|
|
1476
|
+
}
|
|
1477
|
+
const skip = offset !== void 0 && Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0;
|
|
1478
|
+
if (skip > 0) plan.skip = skip;
|
|
1479
|
+
if (limit !== void 0) plan.limitToFirst = limit + skip;
|
|
1480
|
+
return plan;
|
|
1481
|
+
}
|
|
1482
|
+
/** The plan as Realtime Database query constraints. */
|
|
1483
|
+
function rtdbConstraints(plan) {
|
|
1484
|
+
const constraints = [];
|
|
1485
|
+
const bounded = plan.equalTo !== void 0 || plan.startAt !== void 0 || plan.startAfter !== void 0 || plan.endAt !== void 0;
|
|
1486
|
+
if (plan.orderByChild !== void 0) constraints.push(orderByChild(plan.orderByChild));
|
|
1487
|
+
else if (bounded) constraints.push(orderByKey());
|
|
1488
|
+
if (plan.equalTo !== void 0) constraints.push(equalTo(plan.equalTo));
|
|
1489
|
+
if (plan.startAt !== void 0) constraints.push(startAt(plan.startAt));
|
|
1490
|
+
if (plan.startAfter !== void 0) constraints.push(startAfter$1(plan.startAfter));
|
|
1491
|
+
if (plan.endAt !== void 0) constraints.push(endAt(plan.endAt));
|
|
1492
|
+
if (plan.limitToFirst !== void 0) constraints.push(limitToFirst(plan.limitToFirst));
|
|
1493
|
+
return constraints;
|
|
1494
|
+
}
|
|
1387
1495
|
function useFirebaseRTDBDelegate({ firebaseApp }) {
|
|
1388
1496
|
return {
|
|
1389
1497
|
key: "firebase_rtdb",
|
|
1390
|
-
fetchCollection: useCallback(async (
|
|
1498
|
+
fetchCollection: useCallback(async (props) => {
|
|
1391
1499
|
if (!firebaseApp) throw new Error("Firebase app not provided");
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
if (entity.exists()) return Object.entries(entity.val()).map(([id, values]) => ({
|
|
1500
|
+
const database = getDatabase(firebaseApp);
|
|
1501
|
+
const plan = planRTDBQuery(props);
|
|
1502
|
+
const entity = await get(query$1(ref$1(database, props.path), ...rtdbConstraints(plan)));
|
|
1503
|
+
if (entity.exists()) return Object.entries(entity.val()).slice(plan.skip ?? 0).map(([id, values]) => ({
|
|
1397
1504
|
...delegateToCMSModel(values),
|
|
1398
1505
|
id
|
|
1399
1506
|
}));
|
|
1400
1507
|
return [];
|
|
1401
1508
|
}, [firebaseApp]),
|
|
1402
|
-
listenCollection: useCallback((
|
|
1509
|
+
listenCollection: useCallback((props) => {
|
|
1403
1510
|
if (!firebaseApp) throw new Error("Firebase app not provided");
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1511
|
+
const database = getDatabase(firebaseApp);
|
|
1512
|
+
const { onUpdate, onError } = props;
|
|
1513
|
+
const plan = planRTDBQuery(props);
|
|
1514
|
+
const unsubscribe = onValue(query$1(ref$1(database, props.path), ...rtdbConstraints(plan)), (entity) => {
|
|
1515
|
+
if (entity.exists()) {
|
|
1516
|
+
const result = Object.entries(entity.val()).slice(plan.skip ?? 0).map(([id, values]) => ({
|
|
1517
|
+
...delegateToCMSModel(values),
|
|
1518
|
+
id
|
|
1519
|
+
}));
|
|
1520
|
+
onUpdate(result);
|
|
1521
|
+
} else onUpdate([]);
|
|
1522
|
+
}, (error) => onError?.(error));
|
|
1411
1523
|
return () => unsubscribe();
|
|
1412
1524
|
}, [firebaseApp]),
|
|
1413
1525
|
fetchOne: useCallback(async ({ path, id }) => {
|
|
@@ -1512,6 +1624,20 @@ function useRecaptcha() {
|
|
|
1512
1624
|
//#endregion
|
|
1513
1625
|
//#region src/hooks/useBuildUserManagement.tsx
|
|
1514
1626
|
/**
|
|
1627
|
+
* Decide whether a user may access the CMS, given the state of the user
|
|
1628
|
+
* management collection.
|
|
1629
|
+
*
|
|
1630
|
+
* A plain function rather than logic inside the gate callback so the states
|
|
1631
|
+
* that must stay distinct can be asserted without a React render.
|
|
1632
|
+
*/
|
|
1633
|
+
function resolveAccessDecision({ loading, usersError, users, user }) {
|
|
1634
|
+
if (loading) return "loading";
|
|
1635
|
+
if (!user) return "no-user";
|
|
1636
|
+
if (usersError) return "users-unreadable";
|
|
1637
|
+
if (users.length === 0) return "bootstrap";
|
|
1638
|
+
return users.some((u) => u.email?.toLowerCase() === user.email?.toLowerCase()) ? "known-user" : "unknown-user";
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1515
1641
|
* This hook is used to build a user management object that can be used to
|
|
1516
1642
|
* manage users and roles in a Firestore backend.
|
|
1517
1643
|
* @param authController
|
|
@@ -1675,17 +1801,27 @@ function useBuildUserManagement({ authController, dataSourceDelegate, roles: rol
|
|
|
1675
1801
|
return mgmtUser.roles;
|
|
1676
1802
|
}, [usersWithRoleIds]);
|
|
1677
1803
|
const accessGate = useCallback(({ user }) => {
|
|
1678
|
-
|
|
1679
|
-
|
|
1804
|
+
const decision = resolveAccessDecision({
|
|
1805
|
+
loading,
|
|
1806
|
+
usersError,
|
|
1807
|
+
users,
|
|
1808
|
+
user
|
|
1809
|
+
});
|
|
1810
|
+
if (decision === "loading") return false;
|
|
1811
|
+
if (decision === "no-user") {
|
|
1680
1812
|
console.warn("User is null, returning");
|
|
1681
1813
|
return false;
|
|
1682
1814
|
}
|
|
1683
|
-
if (
|
|
1815
|
+
if (decision === "users-unreadable") {
|
|
1816
|
+
console.error("Denying access: the user management collection could not be read", usersError);
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
if (decision === "bootstrap") {
|
|
1684
1820
|
console.warn("No users created yet");
|
|
1685
1821
|
return true;
|
|
1686
1822
|
}
|
|
1687
1823
|
const mgmtUser = users.find((u) => u.email?.toLowerCase() === user?.email?.toLowerCase());
|
|
1688
|
-
if (mgmtUser) {
|
|
1824
|
+
if (decision === "known-user" && mgmtUser && user) {
|
|
1689
1825
|
const needsUidUpdate = mgmtUser.uid !== user.uid;
|
|
1690
1826
|
const needsPhotoUpdate = user.photoURL && mgmtUser.photoURL !== user.photoURL;
|
|
1691
1827
|
if (needsUidUpdate || needsPhotoUpdate) {
|
|
@@ -1704,7 +1840,11 @@ function useBuildUserManagement({ authController, dataSourceDelegate, roles: rol
|
|
|
1704
1840
|
return true;
|
|
1705
1841
|
}
|
|
1706
1842
|
throw Error("Could not find a user with the provided email in the user management system.");
|
|
1707
|
-
}, [
|
|
1843
|
+
}, [
|
|
1844
|
+
loading,
|
|
1845
|
+
users,
|
|
1846
|
+
usersError
|
|
1847
|
+
]);
|
|
1708
1848
|
const userRoles = authController.user ? defineRolesFor(authController.user) : void 0;
|
|
1709
1849
|
const isAdmin = (userRoles ?? []).some((r) => r === "admin");
|
|
1710
1850
|
useEffect(() => {
|
|
@@ -2649,6 +2789,6 @@ function RebaseFirebaseApp({ name, logo, logoDark, accessGate, collections, view
|
|
|
2649
2789
|
}) });
|
|
2650
2790
|
}
|
|
2651
2791
|
//#endregion
|
|
2652
|
-
export { FirebaseLoginView, LoginButton, RECAPTCHA_CONTAINER_ID, RebaseFirebaseApp, buildCollectionId, buildExternalSearchController, buildPineconeSearchController, buildRebaseSearchController, docToCollection, docsToCollectionTree, firestoreToRebaseModel, getFirestoreDataInPath, localSearchControllerBuilder, performAlgoliaTextSearch, performPineconeTextSearch, rebaseToFirestoreModel, useAppCheck, useBuildUserManagement, useFirebaseAccessGate, useFirebaseAuthController, useFirebaseRTDBDelegate, useFirebaseStorageSource, useFirestoreDriver, useInitialiseFirebase, useRecaptcha };
|
|
2792
|
+
export { FirebaseLoginView, LoginButton, RECAPTCHA_CONTAINER_ID, RebaseFirebaseApp, buildCollectionId, buildExternalSearchController, buildPineconeSearchController, buildRebaseSearchController, docToCollection, docsToCollectionTree, firestoreToRebaseModel, getFirestoreDataInPath, localSearchControllerBuilder, performAlgoliaTextSearch, performPineconeTextSearch, planRTDBQuery, rebaseToFirestoreModel, resolveAccessDecision, resolveOffsetWindow, resolveRoleRefresh, useAppCheck, useBuildUserManagement, useFirebaseAccessGate, useFirebaseAuthController, useFirebaseRTDBDelegate, useFirebaseStorageSource, useFirestoreDriver, useInitialiseFirebase, useRecaptcha };
|
|
2653
2793
|
|
|
2654
2794
|
//# sourceMappingURL=index.es.js.map
|