@rebasepro/firebase 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +4 -0
- package/package.json +34 -20
- package/src/components/FirebaseLoginView.tsx +0 -704
- package/src/components/RebaseFirebaseApp.tsx +0 -293
- package/src/components/RebaseFirebaseAppProps.tsx +0 -177
- package/src/components/index.ts +0 -3
- package/src/components/social_icons.tsx +0 -135
- package/src/hooks/index.ts +0 -9
- package/src/hooks/useAppCheck.ts +0 -101
- package/src/hooks/useBuildUserManagement.tsx +0 -429
- package/src/hooks/useFirebaseAccessGate.tsx +0 -146
- package/src/hooks/useFirebaseAuthController.ts +0 -357
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +0 -423
- package/src/hooks/useFirebaseStorageSource.ts +0 -207
- package/src/hooks/useFirestoreDriver.ts +0 -855
- package/src/hooks/useInitialiseFirebase.ts +0 -132
- package/src/hooks/useRecaptcha.tsx +0 -28
- package/src/index.ts +0 -4
- package/src/types/appcheck.ts +0 -11
- package/src/types/auth.tsx +0 -75
- package/src/types/index.ts +0 -3
- package/src/types/text_search.ts +0 -42
- package/src/utils/algolia.ts +0 -27
- package/src/utils/collections_firestore.ts +0 -150
- package/src/utils/database.ts +0 -39
- package/src/utils/index.ts +0 -7
- package/src/utils/local_text_search_controller.ts +0 -143
- package/src/utils/pinecone.ts +0 -75
- package/src/utils/rebase_search_controller.ts +0 -357
- package/src/utils/text_search_controller.ts +0 -34
package/src/hooks/useAppCheck.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
|
-
import { AppCheck, getToken, initializeAppCheck } from "firebase/app-check";
|
|
4
|
-
import { FirebaseApp } from "firebase/app";
|
|
5
|
-
import { AppCheckOptions } from "../types";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @group Firebase
|
|
9
|
-
*/
|
|
10
|
-
export interface InitializeAppCheckProps {
|
|
11
|
-
firebaseApp?: FirebaseApp;
|
|
12
|
-
options?: AppCheckOptions;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface InitializeAppCheckResult {
|
|
16
|
-
loading: boolean;
|
|
17
|
-
appCheckVerified?: boolean;
|
|
18
|
-
error?: unknown;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Function used to initialise Firebase App Check.
|
|
23
|
-
*
|
|
24
|
-
* @group Firebase
|
|
25
|
-
*/
|
|
26
|
-
export function useAppCheck({
|
|
27
|
-
firebaseApp,
|
|
28
|
-
options
|
|
29
|
-
}: InitializeAppCheckProps): InitializeAppCheckResult {
|
|
30
|
-
if (options?.debugToken) {
|
|
31
|
-
Object.assign(window, {
|
|
32
|
-
FIREBASE_APPCHECK_DEBUG_TOKEN: options?.debugToken
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const [appCheckLoading, setAppCheckLoading] = React.useState<boolean>(false);
|
|
37
|
-
const [appCheckVerified, setAppCheckVerified] = React.useState<boolean | undefined>(undefined);
|
|
38
|
-
const [error, setError] = React.useState<unknown>();
|
|
39
|
-
|
|
40
|
-
const initialCheck = useRef<boolean>(false);
|
|
41
|
-
|
|
42
|
-
const verifyToken = useCallback(async (appCheck: AppCheck) => {
|
|
43
|
-
console.debug("Verifying App Check token...", appCheck);
|
|
44
|
-
try {
|
|
45
|
-
const token = await getToken(appCheck, options?.forceRefresh);
|
|
46
|
-
console.debug("App Check token:", token);
|
|
47
|
-
if (!token) {
|
|
48
|
-
setError("App Check failed.");
|
|
49
|
-
setAppCheckVerified(false);
|
|
50
|
-
} else {
|
|
51
|
-
setAppCheckVerified(true);
|
|
52
|
-
console.debug("App Check success.");
|
|
53
|
-
}
|
|
54
|
-
} catch (e: unknown) {
|
|
55
|
-
console.error("App Check error:", e);
|
|
56
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
57
|
-
}
|
|
58
|
-
}, [options?.forceRefresh]);
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (!options) return;
|
|
62
|
-
if (!firebaseApp) return;
|
|
63
|
-
if (appCheckVerified !== undefined) return;
|
|
64
|
-
if (initialCheck.current) return;
|
|
65
|
-
|
|
66
|
-
setAppCheckLoading(true);
|
|
67
|
-
|
|
68
|
-
const {
|
|
69
|
-
provider,
|
|
70
|
-
isTokenAutoRefreshEnabled
|
|
71
|
-
} = options;
|
|
72
|
-
|
|
73
|
-
removeCurrentAppCheckDiv();
|
|
74
|
-
|
|
75
|
-
const appCheck = initializeAppCheck(firebaseApp, {
|
|
76
|
-
provider,
|
|
77
|
-
isTokenAutoRefreshEnabled
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
verifyToken(appCheck)
|
|
81
|
-
.then(() => {
|
|
82
|
-
setAppCheckLoading(false);
|
|
83
|
-
});
|
|
84
|
-
initialCheck.current = true;
|
|
85
|
-
}, [appCheckVerified, firebaseApp, options, verifyToken]);
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
loading: appCheckLoading,
|
|
89
|
-
appCheckVerified,
|
|
90
|
-
error
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
function removeCurrentAppCheckDiv() {
|
|
96
|
-
const div = document.getElementById("fire_app_check_[DEFAULT]");
|
|
97
|
-
if (div) {
|
|
98
|
-
div.remove();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect } from "react";
|
|
2
|
-
import { deepEqual as equal } from "fast-equals";
|
|
3
|
-
|
|
4
|
-
import { removeUndefined } from "@rebasepro/utils";
|
|
5
|
-
import {
|
|
6
|
-
DataDriver,
|
|
7
|
-
Entity,
|
|
8
|
-
User
|
|
9
|
-
} from "@rebasepro/types";
|
|
10
|
-
import {
|
|
11
|
-
AuthController
|
|
12
|
-
} from "@rebasepro/cms-types";
|
|
13
|
-
import { FirebaseAccessGate } from "./useFirebaseAccessGate";
|
|
14
|
-
|
|
15
|
-
export interface Role {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
isAdmin?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type UserWithRoleIds<USER extends User = User> = Omit<USER, "roles"> & { roles: string[] };
|
|
22
|
-
|
|
23
|
-
export interface UserManagementDelegateParams<CONTROLLER extends AuthController<User> = AuthController<User>> {
|
|
24
|
-
|
|
25
|
-
authController: CONTROLLER;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The delegate in charge of persisting the data.
|
|
29
|
-
*/
|
|
30
|
-
dataSourceDelegate?: DataDriver;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Path where the plugin users configuration is stored.
|
|
34
|
-
* Default: __FIRECMS/config/users
|
|
35
|
-
* You can specify a different path if you want to store the user management configuration in a different place.
|
|
36
|
-
* Please keep in mind that the FireCMS users are not necessarily the same as the Firebase users (but they can be).
|
|
37
|
-
* The path should be relative to the root of the database, and should always have an odd number of segments.
|
|
38
|
-
*/
|
|
39
|
-
usersPath?: string;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Path where the plugin roles configuration is stored.
|
|
43
|
-
* Default: __FIRECMS/config/roles
|
|
44
|
-
*/
|
|
45
|
-
rolesPath?: string;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The roles that are available in the user management system.
|
|
49
|
-
* If you provide this, the user management system will not fetch the roles from the database.
|
|
50
|
-
*/
|
|
51
|
-
roles?: Role[];
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* If there are no roles in the database, provide a button to create the default roles.
|
|
55
|
-
*/
|
|
56
|
-
allowDefaultRolesCreation?: boolean;
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Why the access gate answered the way it did.
|
|
62
|
-
*
|
|
63
|
-
* `users-unreadable` is a state of its own because it used to be
|
|
64
|
-
* indistinguishable from `bootstrap`: the users listener's `onError` empties
|
|
65
|
-
* the user list, so a `permission-denied` on the users path — a rules
|
|
66
|
-
* misconfiguration, a rules deploy that has not landed, a renamed path —
|
|
67
|
-
* reached the gate as "no users created yet" and every authenticated user was
|
|
68
|
-
* let in. An unreadable list is not an empty one, and only one of the two may
|
|
69
|
-
* open the door.
|
|
70
|
-
*/
|
|
71
|
-
export type AccessDecision =
|
|
72
|
-
| "loading"
|
|
73
|
-
| "no-user"
|
|
74
|
-
| "users-unreadable"
|
|
75
|
-
| "bootstrap"
|
|
76
|
-
| "known-user"
|
|
77
|
-
| "unknown-user";
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Decide whether a user may access the CMS, given the state of the user
|
|
81
|
-
* management collection.
|
|
82
|
-
*
|
|
83
|
-
* A plain function rather than logic inside the gate callback so the states
|
|
84
|
-
* that must stay distinct can be asserted without a React render.
|
|
85
|
-
*/
|
|
86
|
-
export function resolveAccessDecision({
|
|
87
|
-
loading,
|
|
88
|
-
usersError,
|
|
89
|
-
users,
|
|
90
|
-
user
|
|
91
|
-
}: {
|
|
92
|
-
loading: boolean;
|
|
93
|
-
usersError?: Error;
|
|
94
|
-
users: { email?: string | null }[];
|
|
95
|
-
user: { email?: string | null } | null;
|
|
96
|
-
}): AccessDecision {
|
|
97
|
-
if (loading) return "loading";
|
|
98
|
-
if (!user) return "no-user";
|
|
99
|
-
if (usersError) return "users-unreadable";
|
|
100
|
-
if (users.length === 0) return "bootstrap";
|
|
101
|
-
const known = users.some(u => u.email?.toLowerCase() === user.email?.toLowerCase());
|
|
102
|
-
return known ? "known-user" : "unknown-user";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* This hook is used to build a user management object that can be used to
|
|
107
|
-
* manage users and roles in a Firestore backend.
|
|
108
|
-
* @param authController
|
|
109
|
-
* @param dataSourceDelegate
|
|
110
|
-
* @param usersPath
|
|
111
|
-
* @param rolesPath
|
|
112
|
-
* @param roles
|
|
113
|
-
* @param allowDefaultRolesCreation
|
|
114
|
-
*/
|
|
115
|
-
export function useBuildUserManagement<CONTROLLER extends AuthController<User> = AuthController<User>,
|
|
116
|
-
USER extends User = CONTROLLER extends AuthController<infer U> ? U : User>
|
|
117
|
-
({
|
|
118
|
-
authController,
|
|
119
|
-
dataSourceDelegate,
|
|
120
|
-
roles: rolesProp,
|
|
121
|
-
usersPath = "__FIRECMS/config/users",
|
|
122
|
-
rolesPath = "__FIRECMS/config/roles",
|
|
123
|
-
allowDefaultRolesCreation
|
|
124
|
-
}: UserManagementDelegateParams<CONTROLLER>) {
|
|
125
|
-
|
|
126
|
-
if (!authController) {
|
|
127
|
-
throw Error("useBuildUserManagement: You need to provide an authController since version 3.0.0-beta.11. Check https://firecms.co/docs/pro/migrating_from_v3_beta");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const rolesDefinedInCode = (rolesProp ?? [])?.length > 0;
|
|
131
|
-
const [rolesLoading, setRolesLoading] = React.useState<boolean>(!rolesDefinedInCode);
|
|
132
|
-
const [usersLoading, setUsersLoading] = React.useState<boolean>(true);
|
|
133
|
-
const [roles, setRoles] = React.useState<Role[]>(rolesProp ?? []);
|
|
134
|
-
const [usersWithRoleIds, setUsersWithRoleIds] = React.useState<UserWithRoleIds<USER>[]>([]);
|
|
135
|
-
|
|
136
|
-
const users = usersWithRoleIds.map(u => ({
|
|
137
|
-
...u
|
|
138
|
-
}) as USER);
|
|
139
|
-
|
|
140
|
-
const [rolesError, setRolesError] = React.useState<Error | undefined>();
|
|
141
|
-
const [usersError, setUsersError] = React.useState<Error | undefined>();
|
|
142
|
-
|
|
143
|
-
const _usersLoading = usersLoading;
|
|
144
|
-
const _rolesLoading = rolesLoading;
|
|
145
|
-
|
|
146
|
-
const loading = _rolesLoading || _usersLoading;
|
|
147
|
-
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (rolesDefinedInCode) return;
|
|
150
|
-
if (!dataSourceDelegate || !rolesPath) return;
|
|
151
|
-
if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) return;
|
|
152
|
-
if (authController?.initialLoading) return;
|
|
153
|
-
|
|
154
|
-
setRolesLoading(true);
|
|
155
|
-
return dataSourceDelegate.listenCollection?.({
|
|
156
|
-
path: rolesPath,
|
|
157
|
-
onUpdate(rows: Record<string, unknown>[]): void {
|
|
158
|
-
setRolesError(undefined);
|
|
159
|
-
console.debug("Updating roles", rows);
|
|
160
|
-
try {
|
|
161
|
-
const newRoles = rowsToRoles(rows);
|
|
162
|
-
if (!equal(newRoles, roles)) {
|
|
163
|
-
setRoles(newRoles);
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {
|
|
166
|
-
setRoles([]);
|
|
167
|
-
console.error("Error loading roles", e);
|
|
168
|
-
setRolesError(e as Error);
|
|
169
|
-
}
|
|
170
|
-
setRolesLoading(false);
|
|
171
|
-
},
|
|
172
|
-
onError(e: unknown): void {
|
|
173
|
-
setRoles([]);
|
|
174
|
-
console.error("Error loading roles", e);
|
|
175
|
-
setRolesError(e instanceof Error ? e : new Error(String(e)));
|
|
176
|
-
setRolesLoading(false);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
}, [rolesDefinedInCode, dataSourceDelegate?.initialised, authController?.initialLoading, authController?.user?.uid, rolesPath]);
|
|
181
|
-
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
if (!dataSourceDelegate || !usersPath) return;
|
|
184
|
-
if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (authController?.initialLoading) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
setUsersLoading(true);
|
|
192
|
-
return dataSourceDelegate.listenCollection?.({
|
|
193
|
-
path: usersPath,
|
|
194
|
-
onUpdate(rows: Record<string, unknown>[]): void {
|
|
195
|
-
console.debug("Updating users", rows);
|
|
196
|
-
setUsersError(undefined);
|
|
197
|
-
try {
|
|
198
|
-
const newUsers = rowsToUsers(rows) as UserWithRoleIds<USER>[];
|
|
199
|
-
// if (!equal(newUsers, usersWithRoleIds))
|
|
200
|
-
setUsersWithRoleIds(newUsers);
|
|
201
|
-
} catch (e) {
|
|
202
|
-
setUsersWithRoleIds([]);
|
|
203
|
-
console.error("Error loading users", e);
|
|
204
|
-
setUsersError(e as Error);
|
|
205
|
-
}
|
|
206
|
-
setUsersLoading(false);
|
|
207
|
-
},
|
|
208
|
-
onError(e: unknown): void {
|
|
209
|
-
console.error("Error loading users", e);
|
|
210
|
-
setUsersWithRoleIds([]);
|
|
211
|
-
setUsersError(e instanceof Error ? e : new Error(String(e)));
|
|
212
|
-
setUsersLoading(false);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
}, [dataSourceDelegate?.initialised, authController?.initialLoading, authController?.user?.uid, usersPath]);
|
|
217
|
-
|
|
218
|
-
const saveUser = useCallback(async (user: USER): Promise<USER> => {
|
|
219
|
-
if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
|
|
220
|
-
if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
|
|
221
|
-
|
|
222
|
-
console.debug("Persisting user", user);
|
|
223
|
-
|
|
224
|
-
const roleIds = user.roles;
|
|
225
|
-
const email = user.email?.toLowerCase().trim();
|
|
226
|
-
if (!email) throw Error("Email is required");
|
|
227
|
-
|
|
228
|
-
const userExists = users.find(u => u.email?.toLowerCase() === email);
|
|
229
|
-
const data = {
|
|
230
|
-
...user,
|
|
231
|
-
roles: roleIds ?? [],
|
|
232
|
-
...(userExists ? {} : { created_on: new Date() })
|
|
233
|
-
};
|
|
234
|
-
// delete the previous user entry if it exists and the uid has changed
|
|
235
|
-
if (userExists && userExists.uid !== user.uid) {
|
|
236
|
-
const row = {
|
|
237
|
-
values: {},
|
|
238
|
-
path: usersPath,
|
|
239
|
-
id: userExists.uid
|
|
240
|
-
}
|
|
241
|
-
await dataSourceDelegate.delete({ row })
|
|
242
|
-
.then(() => {
|
|
243
|
-
console.debug("Deleted previous user", userExists);
|
|
244
|
-
})
|
|
245
|
-
.catch(e => {
|
|
246
|
-
console.error("Error deleting user", e);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return dataSourceDelegate.save({
|
|
252
|
-
status: "existing",
|
|
253
|
-
path: usersPath,
|
|
254
|
-
id: email,
|
|
255
|
-
values: removeUndefined(data) as Record<string, unknown>
|
|
256
|
-
}).then(() => user);
|
|
257
|
-
}, [usersPath, dataSourceDelegate?.initialised]);
|
|
258
|
-
|
|
259
|
-
const saveRole = useCallback((role: Role): Promise<void> => {
|
|
260
|
-
if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
|
|
261
|
-
if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
|
|
262
|
-
console.debug("Persisting role", role);
|
|
263
|
-
const {
|
|
264
|
-
id,
|
|
265
|
-
...roleData
|
|
266
|
-
} = role;
|
|
267
|
-
return dataSourceDelegate.save({
|
|
268
|
-
status: "existing",
|
|
269
|
-
path: rolesPath,
|
|
270
|
-
id: id,
|
|
271
|
-
values: removeUndefined(roleData) as Record<string, unknown>
|
|
272
|
-
}).then(() => {
|
|
273
|
-
return;
|
|
274
|
-
});
|
|
275
|
-
}, [rolesPath, dataSourceDelegate?.initialised]);
|
|
276
|
-
|
|
277
|
-
const deleteUser = useCallback(async (user: User): Promise<void> => {
|
|
278
|
-
if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
|
|
279
|
-
if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
|
|
280
|
-
console.debug("Deleting", user);
|
|
281
|
-
const { uid } = user;
|
|
282
|
-
const row = {
|
|
283
|
-
path: usersPath,
|
|
284
|
-
id: uid,
|
|
285
|
-
values: {}
|
|
286
|
-
};
|
|
287
|
-
await dataSourceDelegate.delete({ row })
|
|
288
|
-
}, [usersPath, dataSourceDelegate?.initialised]);
|
|
289
|
-
|
|
290
|
-
const deleteRole = useCallback(async (role: Role): Promise<void> => {
|
|
291
|
-
if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
|
|
292
|
-
if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
|
|
293
|
-
console.debug("Deleting", role);
|
|
294
|
-
const { id } = role;
|
|
295
|
-
const row = {
|
|
296
|
-
path: rolesPath,
|
|
297
|
-
id: id,
|
|
298
|
-
values: {}
|
|
299
|
-
};
|
|
300
|
-
await dataSourceDelegate.delete({ row })
|
|
301
|
-
}, [rolesPath, dataSourceDelegate?.initialised]);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const defineRolesFor: ((user: User) => string[] | undefined) = useCallback((user) => {
|
|
305
|
-
if (!usersWithRoleIds) throw Error("Users not loaded");
|
|
306
|
-
const mgmtUser = usersWithRoleIds.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
|
|
307
|
-
if (!mgmtUser || !mgmtUser.roles) return undefined;
|
|
308
|
-
return mgmtUser.roles;
|
|
309
|
-
}, [usersWithRoleIds]);
|
|
310
|
-
|
|
311
|
-
const accessGate: FirebaseAccessGate<USER> = useCallback(({ user }) => {
|
|
312
|
-
|
|
313
|
-
const decision = resolveAccessDecision({
|
|
314
|
-
loading,
|
|
315
|
-
usersError,
|
|
316
|
-
users,
|
|
317
|
-
user
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
if (decision === "loading") {
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (decision === "no-user") {
|
|
325
|
-
console.warn("User is null, returning");
|
|
326
|
-
return false;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (decision === "users-unreadable") {
|
|
330
|
-
// The users collection could not be read, so we do not know whether
|
|
331
|
-
// this user is in it. Denying is the only safe reading: the
|
|
332
|
-
// bootstrap branch below would otherwise admit everyone.
|
|
333
|
-
console.error("Denying access: the user management collection could not be read", usersError);
|
|
334
|
-
return false;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (decision === "bootstrap") {
|
|
338
|
-
console.warn("No users created yet");
|
|
339
|
-
return true; // If there are no users created yet, we allow access to every user
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
|
|
343
|
-
if (decision === "known-user" && mgmtUser && user) {
|
|
344
|
-
// check if the uid or photoURL needs to be updated in the user management system
|
|
345
|
-
const needsUidUpdate = mgmtUser.uid !== user.uid;
|
|
346
|
-
const needsPhotoUpdate = user.photoURL && mgmtUser.photoURL !== user.photoURL;
|
|
347
|
-
|
|
348
|
-
if (needsUidUpdate || needsPhotoUpdate) {
|
|
349
|
-
const updateReason = needsUidUpdate ? "uid" : "photoURL";
|
|
350
|
-
console.debug(`User ${updateReason} has changed, updating user in user management system`);
|
|
351
|
-
saveUser({
|
|
352
|
-
...mgmtUser,
|
|
353
|
-
uid: user.uid,
|
|
354
|
-
...(needsPhotoUpdate ? { photoURL: user.photoURL } : {})
|
|
355
|
-
}).then(() => {
|
|
356
|
-
console.debug("User updated in user management system", mgmtUser);
|
|
357
|
-
}).catch(e => {
|
|
358
|
-
console.error("Error updating user in user management system", e);
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
console.debug("User found in user management system", mgmtUser);
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
throw Error("Could not find a user with the provided email in the user management system.");
|
|
366
|
-
}, [loading, users, usersError]);
|
|
367
|
-
|
|
368
|
-
const userRoles = authController.user ? defineRolesFor(authController.user) : undefined;
|
|
369
|
-
const isAdmin = (userRoles ?? []).some(r => r === "admin");
|
|
370
|
-
|
|
371
|
-
const userRoleIds = userRoles;
|
|
372
|
-
useEffect(() => {
|
|
373
|
-
console.debug("Setting user roles", {
|
|
374
|
-
userRoles,
|
|
375
|
-
roles
|
|
376
|
-
});
|
|
377
|
-
authController.setUserRoles?.(userRoles ?? []);
|
|
378
|
-
}, [userRoleIds]);
|
|
379
|
-
|
|
380
|
-
const getUser = useCallback((uid: string): USER | null => {
|
|
381
|
-
if (!users) return null;
|
|
382
|
-
const user = users.find(u => u.uid === uid);
|
|
383
|
-
return user ?? null;
|
|
384
|
-
}, [users]);
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
loading,
|
|
388
|
-
roles,
|
|
389
|
-
users,
|
|
390
|
-
saveUser,
|
|
391
|
-
saveRole,
|
|
392
|
-
rolesError,
|
|
393
|
-
deleteUser,
|
|
394
|
-
deleteRole,
|
|
395
|
-
usersError,
|
|
396
|
-
isAdmin,
|
|
397
|
-
allowDefaultRolesCreation: allowDefaultRolesCreation === undefined ? true : allowDefaultRolesCreation,
|
|
398
|
-
defineRolesFor,
|
|
399
|
-
accessGate,
|
|
400
|
-
...authController,
|
|
401
|
-
initialLoading: authController.initialLoading || loading,
|
|
402
|
-
userRoles: userRoles,
|
|
403
|
-
getUser,
|
|
404
|
-
user: authController.user ? {
|
|
405
|
-
...authController.user,
|
|
406
|
-
roles: userRoles
|
|
407
|
-
} : null
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const rowsToUsers = (rows: Record<string, unknown>[]): (UserWithRoleIds)[] => {
|
|
412
|
-
return rows.map((row) => {
|
|
413
|
-
const { id, ...data } = row;
|
|
414
|
-
const newVar = {
|
|
415
|
-
...data,
|
|
416
|
-
uid: id,
|
|
417
|
-
created_on: data.created_on as Date | undefined,
|
|
418
|
-
updated_on: data.updated_on as Date | undefined
|
|
419
|
-
};
|
|
420
|
-
return newVar as unknown as UserWithRoleIds;
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const rowsToRoles = (rows: Record<string, unknown>[]): Role[] => {
|
|
425
|
-
return rows.map((row) => ({
|
|
426
|
-
...row,
|
|
427
|
-
id: row.id
|
|
428
|
-
} as unknown as Role));
|
|
429
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
3
|
-
import { deepEqual as equal } from "fast-equals";
|
|
4
|
-
|
|
5
|
-
import { RebaseData, StorageSource, User } from "@rebasepro/types";
|
|
6
|
-
import { AuthController } from "@rebasepro/cms-types";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Client-side gate that decides whether a Firebase-authenticated user
|
|
10
|
-
* is allowed to access the CMS UI.
|
|
11
|
-
*
|
|
12
|
-
* Return `true` to allow access or `false` / throw to deny.
|
|
13
|
-
* @group Firebase
|
|
14
|
-
*/
|
|
15
|
-
export type FirebaseAccessGate<USER extends User = User> = (props: {
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Logged-in user or null
|
|
19
|
-
*/
|
|
20
|
-
user: USER | null;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* AuthController
|
|
24
|
-
*/
|
|
25
|
-
authController: AuthController<USER>;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Unified data access API
|
|
29
|
-
*/
|
|
30
|
-
data: RebaseData;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Used storage implementation
|
|
34
|
-
*/
|
|
35
|
-
storageSource: StorageSource;
|
|
36
|
-
|
|
37
|
-
}) => boolean | Promise<boolean>;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Hook that evaluates a {@link FirebaseAccessGate} callback after
|
|
41
|
-
* the user logs in and gates access to the main CMS view.
|
|
42
|
-
*
|
|
43
|
-
* @group Firebase
|
|
44
|
-
*/
|
|
45
|
-
export function useFirebaseAccessGate<USER extends User = User>
|
|
46
|
-
({
|
|
47
|
-
disabled,
|
|
48
|
-
authController,
|
|
49
|
-
accessGate,
|
|
50
|
-
storageSource,
|
|
51
|
-
data
|
|
52
|
-
}:
|
|
53
|
-
{
|
|
54
|
-
disabled?: boolean,
|
|
55
|
-
authController: AuthController<USER>,
|
|
56
|
-
accessGate?: boolean | FirebaseAccessGate<USER>,
|
|
57
|
-
data: RebaseData;
|
|
58
|
-
storageSource: StorageSource;
|
|
59
|
-
}): {
|
|
60
|
-
canAccessMainView: boolean,
|
|
61
|
-
authLoading: boolean,
|
|
62
|
-
notAllowedError: unknown,
|
|
63
|
-
authVerified: boolean,
|
|
64
|
-
} {
|
|
65
|
-
|
|
66
|
-
const gateEnabled = Boolean(accessGate);
|
|
67
|
-
|
|
68
|
-
const [authLoading, setAuthLoading] = useState<boolean>(gateEnabled);
|
|
69
|
-
const [notAllowedError, setNotAllowedError] = useState<unknown>(false);
|
|
70
|
-
const [authVerified, setAuthVerified] = useState<boolean>(!gateEnabled || Boolean(authController.loginSkipped));
|
|
71
|
-
|
|
72
|
-
const canAccessMainView = (authVerified) &&
|
|
73
|
-
(!gateEnabled || Boolean(authController.user) || Boolean(authController.loginSkipped)) &&
|
|
74
|
-
!notAllowedError;
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (authController.loginSkipped)
|
|
78
|
-
setAuthVerified(true);
|
|
79
|
-
}, [authController.loginSkipped]);
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* We use this ref to check the authentication only if the user has
|
|
83
|
-
* changed.
|
|
84
|
-
*/
|
|
85
|
-
const checkedUserRef = useRef<User | undefined>(undefined);
|
|
86
|
-
|
|
87
|
-
const checkAccess = useCallback(async () => {
|
|
88
|
-
|
|
89
|
-
if (disabled) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (authController.initialLoading) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!authController.user && !authController.loginSkipped) {
|
|
98
|
-
checkedUserRef.current = undefined;
|
|
99
|
-
setAuthLoading(false);
|
|
100
|
-
setAuthVerified(false);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const delegateUser = authController.user;
|
|
105
|
-
|
|
106
|
-
if (accessGate instanceof Function && delegateUser && !equal(checkedUserRef.current?.uid, delegateUser.uid)) {
|
|
107
|
-
setAuthLoading(true);
|
|
108
|
-
try {
|
|
109
|
-
const allowed = await accessGate({
|
|
110
|
-
user: delegateUser,
|
|
111
|
-
authController,
|
|
112
|
-
data,
|
|
113
|
-
storageSource
|
|
114
|
-
});
|
|
115
|
-
if (!allowed) {
|
|
116
|
-
authController.signOut();
|
|
117
|
-
setNotAllowedError(true);
|
|
118
|
-
}
|
|
119
|
-
} catch (e) {
|
|
120
|
-
setNotAllowedError(e);
|
|
121
|
-
authController.signOut();
|
|
122
|
-
}
|
|
123
|
-
setAuthLoading(false);
|
|
124
|
-
setAuthVerified(true);
|
|
125
|
-
checkedUserRef.current = delegateUser;
|
|
126
|
-
} else {
|
|
127
|
-
setAuthLoading(false);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (!authController.initialLoading && !delegateUser) {
|
|
131
|
-
setAuthVerified(true);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
}, [disabled, authController, accessGate, data, storageSource]);
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
checkAccess();
|
|
138
|
-
}, [checkAccess]);
|
|
139
|
-
|
|
140
|
-
return useMemo(() => ({
|
|
141
|
-
canAccessMainView,
|
|
142
|
-
authLoading: gateEnabled && authLoading,
|
|
143
|
-
notAllowedError,
|
|
144
|
-
authVerified
|
|
145
|
-
}), [canAccessMainView, gateEnabled, authLoading, notAllowedError, authVerified]);
|
|
146
|
-
}
|