@edgedev/firebase 1.2.5 → 1.3.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/edgeFirebase.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { initializeApp } from "firebase/app";
2
2
  import { reactive } from "vue";
3
-
4
3
  import {
5
4
  getFirestore,
6
5
  collection,
@@ -20,7 +19,9 @@ import {
20
19
  Query,
21
20
  startAfter,
22
21
  DocumentData,
23
- setDoc
22
+ setDoc,
23
+ updateDoc,
24
+ deleteField
24
25
  } from "firebase/firestore";
25
26
 
26
27
  import {
@@ -31,7 +32,8 @@ import {
31
32
  Persistence,
32
33
  signInWithEmailAndPassword,
33
34
  onAuthStateChanged,
34
- signOut
35
+ signOut,
36
+ createUserWithEmailAndPassword
35
37
  } from "firebase/auth";
36
38
 
37
39
  interface FirestoreQuery {
@@ -57,12 +59,67 @@ interface CollectionDataObject {
57
59
  [key: string]: object;
58
60
  }
59
61
 
62
+ interface permissions {
63
+ assign: boolean;
64
+ read: boolean;
65
+ write: boolean;
66
+ delete: boolean;
67
+ }
68
+
69
+ type action = "assign" | "read" | "write" | "delete";
70
+
71
+ interface role {
72
+ collectionPath: "-" | string; // - is root
73
+ role: "admin" | "user";
74
+ }
75
+
76
+ // TODO: PASSWORD RESET FUNCTION <-- NOT LOGGED IN, PASSWORD UPDATE FUNTION <-- WHEN LOGGED IN, AND USER META UPDATE FUNCTION (ONLY FOR THEMSELVES)
77
+
78
+ interface specialPermission {
79
+ collectionPath: "-" | string; // - is root
80
+ permissions: permissions;
81
+ }
82
+
60
83
  interface UserDataObject {
61
84
  uid: string | null;
62
85
  email: string;
63
86
  loggedIn: boolean;
64
87
  logInError: boolean;
65
88
  logInErrorMessage: string;
89
+ meta: object;
90
+ roles: role[];
91
+ specialPermissions: specialPermission[];
92
+ }
93
+
94
+ interface newUser {
95
+ email: string;
96
+ roles: role[];
97
+ specialPermissions: specialPermission[];
98
+ meta: object;
99
+ }
100
+
101
+ interface user {
102
+ email: string;
103
+ role: "admin" | "user" | null;
104
+ specialPermission: permissions | null;
105
+ userId: string;
106
+ docId: string;
107
+ uid: string;
108
+ last_updated: Date;
109
+ }
110
+
111
+ interface usersByCollection {
112
+ [collectionPath: string]: [user];
113
+ }
114
+ interface userMeta extends newUser {
115
+ docId: string;
116
+ userId: string;
117
+ }
118
+
119
+ interface userRegister {
120
+ email: string;
121
+ password: string;
122
+ meta: object;
66
123
  }
67
124
 
68
125
  interface Credentials {
@@ -84,6 +141,16 @@ interface firebaseConfig {
84
141
  appId: string;
85
142
  }
86
143
 
144
+ interface actionResponse {
145
+ success: boolean;
146
+ message: string;
147
+ }
148
+
149
+ interface permissionStatus {
150
+ canDo: boolean;
151
+ badCollectionPaths: string[];
152
+ }
153
+
87
154
  export const EdgeFirebase = class {
88
155
  constructor(
89
156
  firebaseConfig: firebaseConfig = {
@@ -104,25 +171,86 @@ export const EdgeFirebase = class {
104
171
 
105
172
  private firebaseConfig = null;
106
173
 
107
- // Initialize Firebase
108
174
  public app = null;
109
175
  public auth = null;
110
176
  public db = null;
111
177
 
112
- // Composable to logout
113
- public logOut = (): void => {
114
- signOut(this.auth)
115
- .then(() => {
116
- Object.keys(this.unsubscibe).forEach((key) => {
117
- if (this.unsubscibe[key] instanceof Function) {
118
- this.unsubscibe[key]();
119
- this.unsubscibe[key] = null;
178
+ private initUserMetaPermissions = async (): Promise<void> => {
179
+ updateDoc(doc(this.db, "users", this.user.email), {
180
+ userId: this.user.uid
181
+ });
182
+ this.user.meta = {};
183
+ const docRef = doc(this.db, "users", this.user.email);
184
+ const docSnap = await getDoc(docRef);
185
+ if (docSnap) {
186
+ this.user.meta = docSnap.data().meta;
187
+ const roles: role[] = [];
188
+ if (docSnap.data().roles) {
189
+ for (const collectionPath in docSnap.data().roles) {
190
+ roles.push({
191
+ collectionPath,
192
+ role: docSnap.data().roles[collectionPath].role
193
+ });
194
+ }
195
+ }
196
+ this.user.roles = roles;
197
+
198
+ const specialPermissions: specialPermission[] = [];
199
+ if (docSnap.data().specialPermissions) {
200
+ for (const collectionPath in docSnap.data().specialPermissions) {
201
+ specialPermissions.push({
202
+ collectionPath,
203
+ permissions:
204
+ docSnap.data().specialPermissions[collectionPath].permissions
205
+ });
206
+ }
207
+ }
208
+ this.user.specialPermissions = specialPermissions;
209
+ }
210
+ const metaUnsubscribe = onSnapshot(
211
+ doc(this.db, "users", this.user.email),
212
+ (doc) => {
213
+ if (!doc.exists()) {
214
+ this.setUser({
215
+ email: this.user.email,
216
+ roles: [],
217
+ specialPermissions: [],
218
+ meta: {}
219
+ });
220
+ this.user.meta = {};
221
+ } else {
222
+ this.user.meta = doc.data().meta;
223
+ const roles: role[] = [];
224
+ if (doc.data().roles) {
225
+ for (const collectionPath in doc.data().roles) {
226
+ roles.push({
227
+ collectionPath,
228
+ role: doc.data().roles[collectionPath].role
229
+ });
230
+ }
120
231
  }
121
- });
122
- })
123
- .catch(() => {
124
- // Do nothing
125
- });
232
+ this.user.roles = roles;
233
+
234
+ const specialPermissions: specialPermission[] = [];
235
+ if (doc.data().specialPermissions) {
236
+ for (const collectionPath in doc.data().specialPermissions) {
237
+ specialPermissions.push({
238
+ collectionPath,
239
+ permissions:
240
+ doc.data().specialPermissions[collectionPath].permissions
241
+ });
242
+ }
243
+ }
244
+ this.user.specialPermissions = specialPermissions;
245
+ }
246
+ }
247
+ );
248
+ this.unsubscibe.userMeta = metaUnsubscribe;
249
+ };
250
+
251
+ private startUserMetaSync = async (): Promise<void> => {
252
+ await this.initUserMetaPermissions();
253
+ this.user.loggedIn = true;
126
254
  };
127
255
 
128
256
  private setOnAuthStateChanged = (): void => {
@@ -130,9 +258,10 @@ export const EdgeFirebase = class {
130
258
  if (userAuth) {
131
259
  this.user.email = userAuth.email;
132
260
  this.user.uid = userAuth.uid;
133
- this.user.loggedIn = true;
134
261
  this.user.logInError = false;
135
262
  this.user.logInErrorMessage = "";
263
+
264
+ this.startUserMetaSync();
136
265
  } else {
137
266
  this.user.email = "";
138
267
  this.user.uid = null;
@@ -143,6 +272,320 @@ export const EdgeFirebase = class {
143
272
  });
144
273
  };
145
274
 
275
+ public registerUser = async (
276
+ userRegister: userRegister
277
+ ): Promise<actionResponse> => {
278
+ const userRef = doc(this.db, "users", userRegister.email);
279
+ const userSnap = await getDoc(userRef);
280
+ if (userSnap.exists()) {
281
+ const user = userSnap.data();
282
+ if (user.userId) {
283
+ return this.sendResponse({
284
+ success: false,
285
+ message: "User already registered"
286
+ });
287
+ } else {
288
+ createUserWithEmailAndPassword(
289
+ this.auth,
290
+ userRegister.email,
291
+ userRegister.password
292
+ ).then(() => {
293
+ const metaUpdate = {};
294
+ for (const [key, value] of Object.entries(userRegister.meta)) {
295
+ metaUpdate["meta." + key] = value;
296
+ }
297
+ if (Object.keys(metaUpdate).length > 0) {
298
+ updateDoc(doc(this.db, "users", this.user.email), metaUpdate);
299
+ }
300
+ return this.sendResponse({
301
+ success: true,
302
+ message: ""
303
+ });
304
+ });
305
+ }
306
+ } else {
307
+ return this.sendResponse({
308
+ success: false,
309
+ message: "User doesn't exist"
310
+ });
311
+ }
312
+ };
313
+
314
+ public removeUser = async (email: string): Promise<actionResponse> => {
315
+ const removedFrom = [];
316
+ const userRef = doc(this.db, "users", email);
317
+ const userSnap = await getDoc(userRef);
318
+ if (userSnap.data().roles) {
319
+ for (const collectionPath in userSnap.data().roles) {
320
+ const canAssign = await this.permissionCheck(
321
+ "assign",
322
+ collectionPath.replaceAll("-", "/")
323
+ );
324
+ if (canAssign) {
325
+ this.removeUserRoles(email, collectionPath.replaceAll("-", "/"));
326
+ removedFrom.push(collectionPath.replaceAll("-", "/"));
327
+ }
328
+ }
329
+ }
330
+ if (userSnap.data().specialPermissions) {
331
+ for (const collectionPath in userSnap.data().specialPermissions) {
332
+ const canAssign = await this.permissionCheck(
333
+ "assign",
334
+ collectionPath.replaceAll("-", "/")
335
+ );
336
+ if (canAssign) {
337
+ this.removeUserSpecialPermissions(
338
+ email,
339
+ collectionPath.replaceAll("-", "/")
340
+ );
341
+ removedFrom.push(collectionPath.replaceAll("-", "/"));
342
+ }
343
+ }
344
+ }
345
+ if (removedFrom.length > 0) {
346
+ return this.sendResponse({
347
+ success: true,
348
+ message: ""
349
+ });
350
+ } else {
351
+ return this.sendResponse({
352
+ success: false,
353
+ message: "You do not have permission to remove this user"
354
+ });
355
+ }
356
+ };
357
+
358
+ public setUser = async (newUser: newUser): Promise<actionResponse> => {
359
+ const canAssignRole = await this.multiPermissionCheck(
360
+ "assign",
361
+ newUser.roles
362
+ );
363
+ const canAssignSpecialPermissions = await this.multiPermissionCheck(
364
+ "assign",
365
+ newUser.specialPermissions
366
+ );
367
+ if (canAssignRole.canDo && canAssignSpecialPermissions.canDo) {
368
+ const userMeta: userMeta = {
369
+ docId: newUser.email,
370
+ userId: "",
371
+ email: newUser.email,
372
+ roles: newUser.roles,
373
+ specialPermissions: newUser.specialPermissions,
374
+ meta: newUser.meta
375
+ };
376
+ this.generateUserMeta(userMeta);
377
+ return this.sendResponse({
378
+ success: true,
379
+ message: ""
380
+ });
381
+ } else {
382
+ return this.sendResponse({
383
+ success: false,
384
+ message:
385
+ "Cannot assign role or special permission for collection path(s): " +
386
+ canAssignRole.badCollectionPaths
387
+ .concat(canAssignSpecialPermissions.badCollectionPaths)
388
+ .join(", ")
389
+ });
390
+ }
391
+ };
392
+
393
+ private multiPermissionCheck = async (
394
+ action: action,
395
+ collections = []
396
+ ): Promise<permissionStatus> => {
397
+ let canDo = true;
398
+ const badCollectionPaths = [];
399
+ // if (collections.length === 0) {
400
+ // canDo = false;
401
+ // }
402
+ for (const collection of collections) {
403
+ if (!(await this.permissionCheck(action, collection.collectionPath))) {
404
+ badCollectionPaths.push(collection.collectionPath);
405
+ canDo = false;
406
+ }
407
+ }
408
+ if (!canDo) {
409
+ return {
410
+ canDo: false,
411
+ badCollectionPaths
412
+ };
413
+ } else {
414
+ return {
415
+ canDo: true,
416
+ badCollectionPaths: []
417
+ };
418
+ }
419
+ };
420
+
421
+ private sendResponse = (response: actionResponse): actionResponse => {
422
+ console.log(response);
423
+ return response;
424
+ };
425
+
426
+ private permissionCheck = async (
427
+ action: action,
428
+ collectionPath: string
429
+ ): Promise<boolean> => {
430
+ const collection = collectionPath.split("/");
431
+ let index = collection.length;
432
+ let permissionData = {};
433
+ permissionData = {
434
+ read: false,
435
+ write: false,
436
+ delete: false,
437
+ assign: false
438
+ };
439
+ while (index > 0) {
440
+ if (!permissionData[action]) {
441
+ const collectionArray = JSON.parse(JSON.stringify(collection));
442
+ const permissionCheck = collectionArray.splice(0, index).join("-");
443
+ const role = this.user.roles.find(
444
+ (r) => r.collectionPath === permissionCheck
445
+ );
446
+
447
+ if (role) {
448
+ permissionData = await this.getCollectionPermissions(
449
+ permissionCheck,
450
+ role.role
451
+ );
452
+ }
453
+ const specialPermission = this.user.specialPermissions.find(
454
+ (r) => r.collectionPath === permissionCheck
455
+ );
456
+ if (specialPermission) {
457
+ permissionData = specialPermission.permissions;
458
+ }
459
+ }
460
+ index--;
461
+ }
462
+ if (!permissionData[action]) {
463
+ const rootRole = this.user.roles.find((r) => r.collectionPath === "-");
464
+ if (rootRole) {
465
+ permissionData = await this.getCollectionPermissions(
466
+ "-",
467
+ rootRole.role
468
+ );
469
+ }
470
+ const rootSpecialPermission = this.user.specialPermissions.find(
471
+ (r) => r.collectionPath === "-"
472
+ );
473
+ if (rootSpecialPermission) {
474
+ permissionData = rootSpecialPermission.permissions;
475
+ }
476
+ }
477
+ return permissionData[action];
478
+ };
479
+
480
+ private getCollectionPermissions = async (
481
+ collectionPath: string,
482
+ role: string
483
+ ): Promise<permissions> => {
484
+ const collectionRef = doc(
485
+ this.db,
486
+ "collection-data",
487
+ collectionPath.replaceAll("/", "-")
488
+ );
489
+ const collectionSnap = await getDoc(collectionRef);
490
+
491
+ if (collectionSnap.exists()) {
492
+ const permissionData = collectionSnap.data()[role];
493
+ return {
494
+ read: permissionData.read,
495
+ write: permissionData.write,
496
+ delete: permissionData.delete,
497
+ assign: permissionData.assign
498
+ };
499
+ } else {
500
+ return {
501
+ read: false,
502
+ write: false,
503
+ delete: false,
504
+ assign: false
505
+ };
506
+ }
507
+ };
508
+
509
+ private generateUserMeta = async (userMeta: userMeta): Promise<void> => {
510
+ const roles: role[] = userMeta.roles;
511
+ const specialPermissions: specialPermission[] = userMeta.specialPermissions;
512
+ delete userMeta.roles;
513
+ delete userMeta.specialPermissions;
514
+
515
+ const docRef = doc(this.db, "users", userMeta.docId);
516
+ const docSnap = await getDoc(docRef);
517
+ const docData = docSnap.data();
518
+ const canWrite = await this.permissionCheck("write", "users");
519
+ if (!docData || canWrite) {
520
+ setDoc(doc(this.db, "users", userMeta.docId), userMeta);
521
+ }
522
+ for (const role of roles) {
523
+ await this.generatePermissions(role.collectionPath);
524
+ this.storeUserRoles(userMeta.docId, role.collectionPath, role.role);
525
+ }
526
+ for (const specialPermission of specialPermissions) {
527
+ await this.generatePermissions(specialPermission.collectionPath);
528
+ this.storeUserSpecialPermissions(
529
+ userMeta.docId,
530
+ specialPermission.collectionPath,
531
+ specialPermission.permissions
532
+ );
533
+ }
534
+ };
535
+
536
+ private generatePermissions = async (
537
+ collectionPath: string
538
+ ): Promise<void> => {
539
+ const collection = collectionPath.split("/");
540
+ let index = collection.length;
541
+ while (index > 0) {
542
+ const collectionArray = JSON.parse(JSON.stringify(collection));
543
+ const permissionCheck = collectionArray.splice(0, index).join("/");
544
+ const hasPermissions = await this.collectionExists(permissionCheck);
545
+ const adminPermission: permissions = {
546
+ assign: true,
547
+ read: true,
548
+ write: true,
549
+ delete: true
550
+ };
551
+ const userPermission: permissions = {
552
+ assign: false,
553
+ read: false,
554
+ write: false,
555
+ delete: false
556
+ };
557
+ if (!hasPermissions) {
558
+ await this.storeCollectionPermissions(
559
+ permissionCheck,
560
+ "admin",
561
+ adminPermission
562
+ );
563
+ await this.storeCollectionPermissions(
564
+ permissionCheck,
565
+ "user",
566
+ userPermission
567
+ );
568
+ }
569
+ index = index - 1;
570
+ }
571
+ };
572
+
573
+ // Composable to logout
574
+ public logOut = (): void => {
575
+ signOut(this.auth)
576
+ .then(() => {
577
+ Object.keys(this.unsubscibe).forEach((key) => {
578
+ if (this.unsubscibe[key] instanceof Function) {
579
+ this.unsubscibe[key]();
580
+ this.unsubscibe[key] = null;
581
+ }
582
+ });
583
+ })
584
+ .catch(() => {
585
+ // Do nothing
586
+ });
587
+ };
588
+
146
589
  // Composable to login and set persistence
147
590
  public logIn = (credentials: Credentials, isPersistant = false): void => {
148
591
  this.logOut();
@@ -196,7 +639,10 @@ export const EdgeFirebase = class {
196
639
  email: "",
197
640
  loggedIn: false,
198
641
  logInError: false,
199
- logInErrorMessage: ""
642
+ logInErrorMessage: "",
643
+ meta: {},
644
+ roles: [],
645
+ specialPermissions: []
200
646
  });
201
647
 
202
648
  public getDocData = async (
@@ -210,6 +656,21 @@ export const EdgeFirebase = class {
210
656
  return docData;
211
657
  };
212
658
 
659
+ private collectionExists = async (
660
+ collectionPath: string
661
+ ): Promise<boolean> => {
662
+ const collectionRef = doc(
663
+ this.db,
664
+ "collection-data",
665
+ collectionPath.replaceAll("/", "-")
666
+ );
667
+ const collectionSnap = await getDoc(collectionRef);
668
+ if (collectionSnap.exists()) {
669
+ return true;
670
+ }
671
+ return false;
672
+ };
673
+
213
674
  private getStaticData = async (
214
675
  collectionPath: string,
215
676
  queryList: FirestoreQuery[] = [],
@@ -223,7 +684,6 @@ export const EdgeFirebase = class {
223
684
 
224
685
  const docs = await getDocs(q);
225
686
  const nextLast: DocumentData = docs.docs[docs.docs.length - 1];
226
-
227
687
  docs.forEach((doc) => {
228
688
  const item = doc.data();
229
689
  item.docId = doc.id;
@@ -235,6 +695,8 @@ export const EdgeFirebase = class {
235
695
  // Class for wrapping a getSaticData to handle pagination
236
696
  get SearchStaticData() {
237
697
  const getStaticData = this.getStaticData;
698
+ const permissionCheck = this.permissionCheck;
699
+ const sendResponse = this.sendResponse;
238
700
  return class {
239
701
  private collectionPath = "";
240
702
  private queryList: FirestoreQuery[] = [];
@@ -281,7 +743,7 @@ export const EdgeFirebase = class {
281
743
 
282
744
  private afterNextPrev = async (last): Promise<void> => {
283
745
  let results = await getStaticData(
284
- "users",
746
+ this.collectionPath,
285
747
  this.queryList,
286
748
  this.orderList,
287
749
  this.max,
@@ -301,7 +763,7 @@ export const EdgeFirebase = class {
301
763
  this.results.pagination[this.results.pagination.length - 2].key;
302
764
  }
303
765
  results = await getStaticData(
304
- "users",
766
+ this.collectionPath,
305
767
  this.queryList,
306
768
  this.orderList,
307
769
  this.max,
@@ -335,63 +797,51 @@ export const EdgeFirebase = class {
335
797
  queryList: FirestoreQuery[] = [],
336
798
  orderList: FirestoreOrderBy[] = [],
337
799
  max = 0
338
- ): Promise<void> => {
339
- this.collectionPath = collectionPath;
340
- this.queryList = queryList;
341
- this.orderList = orderList;
342
- this.max = max;
343
- this.results.staticIsLastPage = true;
344
- this.results.staticIsFirstPage = true;
345
- this.results.staticCurrentPage = "";
346
- this.results.pagination = [];
347
- this.results.pagination = [];
348
- this.results.data = {};
349
- const results = await getStaticData(
350
- collectionPath,
351
- queryList,
352
- orderList,
353
- max
354
- );
355
- if (Object.keys(results.data).length > 0) {
356
- this.results.staticIsLastPage = false;
357
- this.results.data = results.data;
358
- this.results.staticCurrentPage = results.next.id;
359
- this.results.pagination.push({
360
- key: results.next.id,
361
- next: results.next
362
- });
363
- } else {
800
+ ): Promise<actionResponse> => {
801
+ const canRead = await permissionCheck("read", collectionPath);
802
+
803
+ if (canRead) {
804
+ this.collectionPath = collectionPath;
805
+ this.queryList = queryList;
806
+ this.orderList = orderList;
807
+ this.max = max;
364
808
  this.results.staticIsLastPage = true;
365
809
  this.results.staticIsFirstPage = true;
810
+ this.results.staticCurrentPage = "";
811
+ this.results.pagination = [];
812
+ this.results.data = {};
813
+ const results = await getStaticData(
814
+ collectionPath,
815
+ queryList,
816
+ orderList,
817
+ max
818
+ );
819
+ if (Object.keys(results.data).length > 0) {
820
+ this.results.staticIsLastPage = false;
821
+ this.results.data = results.data;
822
+ this.results.staticCurrentPage = results.next.id;
823
+ this.results.pagination.push({
824
+ key: results.next.id,
825
+ next: results.next
826
+ });
827
+ } else {
828
+ this.results.staticIsLastPage = true;
829
+ this.results.staticIsFirstPage = true;
830
+ }
831
+ return sendResponse({
832
+ success: true,
833
+ message: ""
834
+ });
835
+ } else {
836
+ return sendResponse({
837
+ success: false,
838
+ message: `You do not have permission to read from "${collectionPath}"`
839
+ });
366
840
  }
367
841
  };
368
842
  };
369
843
  }
370
844
 
371
- // Class for wrapping a getSaticData to handle pagination
372
- public SearchStaticDatas = new (class {})();
373
-
374
- // Composable to start snapshot listener and set unsubscribe function
375
- public startSnapshot = (
376
- collectionPath: string,
377
- queryList: FirestoreQuery[] = [],
378
- orderList: FirestoreOrderBy[] = [],
379
- max = 0
380
- ): void => {
381
- this.data[collectionPath] = {};
382
- const q = this.getQuery(collectionPath, queryList, orderList, max);
383
- const unsubscribe = onSnapshot(q, (querySnapshot) => {
384
- const items = {};
385
- querySnapshot.forEach((doc) => {
386
- const item = doc.data();
387
- item.docId = doc.id;
388
- items[doc.id] = item;
389
- });
390
- this.data[collectionPath] = items;
391
- });
392
- this.unsubscibe[collectionPath] = unsubscribe;
393
- };
394
-
395
845
  private getQuery = (
396
846
  collectionPath: string,
397
847
  queryList: FirestoreQuery[] = [],
@@ -432,47 +882,399 @@ export const EdgeFirebase = class {
432
882
  );
433
883
  };
434
884
 
885
+ public startSnapshot = async (
886
+ collectionPath: string,
887
+ queryList: FirestoreQuery[] = [],
888
+ orderList: FirestoreOrderBy[] = [],
889
+ max = 0
890
+ ): Promise<actionResponse> => {
891
+ const canRead = await this.permissionCheck("read", collectionPath);
892
+ this.data[collectionPath] = {};
893
+ this.unsubscibe[collectionPath] = null;
894
+ if (canRead) {
895
+ const q = this.getQuery(collectionPath, queryList, orderList, max);
896
+ const unsubscribe = onSnapshot(q, (querySnapshot) => {
897
+ const items = {};
898
+ querySnapshot.forEach((doc) => {
899
+ const item = doc.data();
900
+ item.docId = doc.id;
901
+ items[doc.id] = item;
902
+ });
903
+ this.data[collectionPath] = items;
904
+ });
905
+ this.unsubscibe[collectionPath] = unsubscribe;
906
+ return this.sendResponse({
907
+ success: true,
908
+ message: ""
909
+ });
910
+ } else {
911
+ return this.sendResponse({
912
+ success: false,
913
+ message: `You do not have permission to read from "${collectionPath}"`
914
+ });
915
+ }
916
+ };
917
+
918
+ public listCollectionsCanAssign = async (): Promise<string[]> => {
919
+ let collectionPaths = [];
920
+ for (const role of this.user.roles) {
921
+ const canAssign = await this.permissionCheck(
922
+ "assign",
923
+ role.collectionPath
924
+ );
925
+ if (canAssign) {
926
+ collectionPaths.push(role.collectionPath);
927
+ }
928
+ }
929
+ for (const specialPermission of this.user.specialPermissions) {
930
+ const canAssign = await this.permissionCheck(
931
+ "assign",
932
+ specialPermission.collectionPath
933
+ );
934
+ if (canAssign) {
935
+ collectionPaths.push(specialPermission.collectionPath);
936
+ }
937
+ }
938
+ collectionPaths = [...new Set(collectionPaths)];
939
+ let collectionPathList = [];
940
+ for (const collectionPath of collectionPaths) {
941
+ if (collectionPath === "-") {
942
+ const collections = await getDocs(
943
+ collection(this.db, "collection-data")
944
+ );
945
+ collections.forEach((doc) => {
946
+ collectionPathList.push(doc.id);
947
+ });
948
+ } else {
949
+ const collections = await getDocs(
950
+ query(
951
+ collection(this.db, "collection-data"),
952
+ where("collectionPath", ">=", collectionPath),
953
+ where("collectionPath", "<", collectionPath + "\uF8FF")
954
+ )
955
+ );
956
+ collections.forEach((doc) => {
957
+ collectionPathList.push(doc.id);
958
+ });
959
+ }
960
+ }
961
+ collectionPathList = [...new Set(collectionPathList)];
962
+ return collectionPathList;
963
+ };
964
+
965
+ public listUsers = async (): Promise<usersByCollection> => {
966
+ const userList = {};
967
+ const collectionPathList = await this.listCollectionsCanAssign();
968
+ for (const collectionPath of collectionPathList) {
969
+ userList[collectionPath] = [];
970
+ const roleUsers = await getDocs(
971
+ query(
972
+ collection(this.db, "users"),
973
+ where(
974
+ "roles." + collectionPath + ".collectionPath",
975
+ "==",
976
+ collectionPath
977
+ )
978
+ )
979
+ );
980
+ roleUsers.forEach((doc) => {
981
+ const user = doc.data();
982
+ userList[collectionPath].push({
983
+ docId: user.docId,
984
+ email: user.email,
985
+ role: user.roles[collectionPath].role,
986
+ specialPermission: null,
987
+ meta: user.meta,
988
+ last_updated: user.last_updated,
989
+ userId: user.userId,
990
+ uid: user.uid
991
+ });
992
+ });
993
+ const specialPermissionsUsers = await getDocs(
994
+ query(
995
+ collection(this.db, "users"),
996
+ where(
997
+ "specialPermissions." + collectionPath + ".collectionPath",
998
+ "==",
999
+ collectionPath
1000
+ )
1001
+ )
1002
+ );
1003
+ specialPermissionsUsers.forEach((doc) => {
1004
+ const user = doc.data();
1005
+ userList[collectionPath].push({
1006
+ docId: user.docId,
1007
+ email: user.email,
1008
+ role: null,
1009
+ specialPermission:
1010
+ user.specialPermissions[collectionPath].permissions,
1011
+ meta: user.meta,
1012
+ last_updated: user.last_updated,
1013
+ userId: user.userId,
1014
+ uid: user.uid
1015
+ });
1016
+ });
1017
+ }
1018
+ return userList;
1019
+ };
1020
+
1021
+ public removeUserRoles = async (
1022
+ email: string,
1023
+ collectionPath: string
1024
+ ): Promise<actionResponse> => {
1025
+ const canAssign = await this.permissionCheck("assign", collectionPath);
1026
+ if (canAssign) {
1027
+ await updateDoc(doc(this.db, "users/" + email), {
1028
+ ["roles." + collectionPath.replaceAll("/", "-")]: deleteField()
1029
+ });
1030
+ return this.sendResponse({
1031
+ success: true,
1032
+ message: ""
1033
+ });
1034
+ } else {
1035
+ return this.sendResponse({
1036
+ success: false,
1037
+ message:
1038
+ "Cannot remove permissions for collection path: " + collectionPath
1039
+ });
1040
+ }
1041
+ };
1042
+
1043
+ public removeUserSpecialPermissions = async (
1044
+ email: string,
1045
+ collectionPath: string
1046
+ ): Promise<actionResponse> => {
1047
+ const canAssign = await this.permissionCheck("assign", collectionPath);
1048
+ if (canAssign) {
1049
+ await updateDoc(doc(this.db, "users/" + email), {
1050
+ ["specialPermissions." + collectionPath.replaceAll("/", "-")]:
1051
+ deleteField()
1052
+ });
1053
+ return this.sendResponse({
1054
+ success: true,
1055
+ message: ""
1056
+ });
1057
+ } else {
1058
+ return this.sendResponse({
1059
+ success: false,
1060
+ message:
1061
+ "Cannot remove permissions for collection path: " + collectionPath
1062
+ });
1063
+ }
1064
+ };
1065
+
1066
+ public storeUserSpecialPermissions = async (
1067
+ email: string,
1068
+ collectionPath: string,
1069
+ permissions: permissions
1070
+ ): Promise<actionResponse> => {
1071
+ const canAssign = await this.permissionCheck("assign", collectionPath);
1072
+ if (canAssign) {
1073
+ const collectionExists = await this.collectionExists(collectionPath);
1074
+ if (collectionExists) {
1075
+ const permissionItem = {
1076
+ ["specialPermissions." + collectionPath.replaceAll("/", "-")]: {
1077
+ collectionPath: collectionPath.replaceAll("/", "-"),
1078
+ permissions
1079
+ }
1080
+ };
1081
+ updateDoc(doc(this.db, "users/" + email), permissionItem);
1082
+ return this.sendResponse({
1083
+ success: true,
1084
+ message: ""
1085
+ });
1086
+ } else {
1087
+ return this.sendResponse({
1088
+ success: false,
1089
+ message: collectionPath + " is not a valid collection path"
1090
+ });
1091
+ }
1092
+ } else {
1093
+ return this.sendResponse({
1094
+ success: false,
1095
+ message:
1096
+ "Cannot assign permissions for collection path: " + collectionPath
1097
+ });
1098
+ }
1099
+ };
1100
+
1101
+ public storeUserRoles = async (
1102
+ email: string,
1103
+ collectionPath: string,
1104
+ role: "admin" | "user"
1105
+ ): Promise<actionResponse> => {
1106
+ const canAssign = await this.permissionCheck("assign", collectionPath);
1107
+
1108
+ if (canAssign) {
1109
+ if (role === "admin" || role === "user") {
1110
+ const collectionExists = await this.collectionExists(collectionPath);
1111
+ if (collectionExists) {
1112
+ const roleItem = {
1113
+ ["roles." + collectionPath.replaceAll("/", "-")]: {
1114
+ collectionPath: collectionPath.replaceAll("/", "-"),
1115
+ role
1116
+ }
1117
+ };
1118
+
1119
+ updateDoc(doc(this.db, "users/" + email), roleItem);
1120
+ return this.sendResponse({
1121
+ success: true,
1122
+ message: ""
1123
+ });
1124
+ } else {
1125
+ return this.sendResponse({
1126
+ success: false,
1127
+ message: collectionPath + " is not a valid collection path"
1128
+ });
1129
+ }
1130
+ } else {
1131
+ return this.sendResponse({
1132
+ success: false,
1133
+ message: "Role must be either 'admin' or 'user'"
1134
+ });
1135
+ }
1136
+ } else {
1137
+ return this.sendResponse({
1138
+ success: false,
1139
+ message:
1140
+ "Cannot assign permissions for collection path: " + collectionPath
1141
+ });
1142
+ }
1143
+ };
1144
+
1145
+ public storeCollectionPermissions = async (
1146
+ collectionPath: string,
1147
+ role: "admin" | "user",
1148
+ permissions: permissions
1149
+ ): Promise<actionResponse> => {
1150
+ const canAssign = await this.permissionCheck("assign", collectionPath);
1151
+
1152
+ if (canAssign) {
1153
+ if (role === "admin" || role === "user") {
1154
+ const currentTime = new Date().getTime();
1155
+
1156
+ const collectionItem = {
1157
+ collectionPath: collectionPath.replaceAll("/", "-"),
1158
+ docId: collectionPath.replaceAll("/", "-")
1159
+ };
1160
+ const collectionRef = doc(
1161
+ this.db,
1162
+ "collection-data",
1163
+ collectionItem.collectionPath
1164
+ );
1165
+ const collectionSnap = await getDoc(collectionRef);
1166
+ if (!collectionSnap.exists()) {
1167
+ await setDoc(
1168
+ doc(this.db, "collection-data", collectionItem.collectionPath),
1169
+ collectionItem
1170
+ );
1171
+ }
1172
+ await updateDoc(
1173
+ doc(this.db, "collection-data/" + collectionItem.collectionPath),
1174
+ { [role]: permissions, uid: this.user.uid, last_updated: currentTime }
1175
+ );
1176
+
1177
+ return this.sendResponse({
1178
+ success: true,
1179
+ message: ""
1180
+ });
1181
+ } else {
1182
+ return this.sendResponse({
1183
+ success: false,
1184
+ message: "Role must be either 'admin' or 'user'"
1185
+ });
1186
+ }
1187
+ } else {
1188
+ return this.sendResponse({
1189
+ success: false,
1190
+ message:
1191
+ "Cannot assign permissions for collection path: " + collectionPath
1192
+ });
1193
+ }
1194
+ };
1195
+
435
1196
  // Composable to update/add a document
436
1197
  public storeDoc = async (
437
1198
  collectionPath: string,
438
- item: object
439
- ): Promise<void> => {
440
- const cloneItem = JSON.parse(JSON.stringify(item));
441
- const currentTime = new Date().getTime();
442
- cloneItem.last_updated = currentTime;
443
- cloneItem.uid = this.user.uid;
444
- if (!Object.prototype.hasOwnProperty.call(cloneItem, "doc_created_at")) {
445
- cloneItem.doc_created_at = currentTime;
446
- }
447
- if (Object.prototype.hasOwnProperty.call(cloneItem, "docId")) {
448
- const docId = cloneItem.docId;
449
- if (Object.prototype.hasOwnProperty.call(this.data, collectionPath)) {
450
- this.data[collectionPath][docId] = cloneItem;
451
- }
452
- setDoc(doc(this.db, collectionPath, docId), cloneItem);
1199
+ item: object,
1200
+ generatePermissions = true
1201
+ ): Promise<actionResponse> => {
1202
+ const canWrite = await this.permissionCheck("write", collectionPath);
1203
+ if (!canWrite) {
1204
+ return this.sendResponse({
1205
+ success: false,
1206
+ message: `You do not have permission to write to "${collectionPath}"`
1207
+ });
453
1208
  } else {
454
- const docRef = await addDoc(
455
- collection(this.db, collectionPath),
456
- cloneItem
457
- );
458
- if (Object.prototype.hasOwnProperty.call(this.data, collectionPath)) {
459
- this.data[collectionPath][docRef.id] = cloneItem;
1209
+ if (generatePermissions) {
1210
+ collectionPath = collectionPath.replaceAll("-", "_");
1211
+ this.generatePermissions(collectionPath);
1212
+ }
1213
+ const cloneItem = JSON.parse(JSON.stringify(item));
1214
+ const currentTime = new Date().getTime();
1215
+ cloneItem.last_updated = currentTime;
1216
+ cloneItem.uid = this.user.uid;
1217
+ if (!Object.prototype.hasOwnProperty.call(cloneItem, "doc_created_at")) {
1218
+ cloneItem.doc_created_at = currentTime;
1219
+ }
1220
+ if (Object.prototype.hasOwnProperty.call(cloneItem, "docId")) {
1221
+ const docId = cloneItem.docId;
1222
+ const canRead = await this.permissionCheck("read", collectionPath);
1223
+ if (canRead) {
1224
+ if (Object.prototype.hasOwnProperty.call(this.data, collectionPath)) {
1225
+ this.data[collectionPath][docId] = cloneItem;
1226
+ }
1227
+ }
1228
+ setDoc(doc(this.db, collectionPath, docId), cloneItem);
1229
+ } else {
1230
+ const docRef = await addDoc(
1231
+ collection(this.db, collectionPath),
1232
+ cloneItem
1233
+ );
1234
+ const canRead = await this.permissionCheck("read", collectionPath);
1235
+ if (canRead) {
1236
+ if (Object.prototype.hasOwnProperty.call(this.data, collectionPath)) {
1237
+ this.data[collectionPath][docRef.id] = cloneItem;
1238
+ }
1239
+ }
1240
+ this.storeDoc(
1241
+ collectionPath,
1242
+ { ...cloneItem, docId: docRef.id },
1243
+ generatePermissions
1244
+ );
460
1245
  }
461
- this.storeDoc(collectionPath, { ...cloneItem, docId: docRef.id });
1246
+ return this.sendResponse({
1247
+ success: true,
1248
+ message: ""
1249
+ });
462
1250
  }
463
1251
  };
464
1252
 
465
1253
  // Composable to delete a document
466
- public removeDoc = (collectionPath: string, docId: string): void => {
467
- // Just in case getting collection back from firebase is slow:
468
- if (Object.prototype.hasOwnProperty.call(this.data, collectionPath)) {
469
- if (
470
- Object.prototype.hasOwnProperty.call(this.data[collectionPath], docId)
471
- ) {
472
- delete this.data[collectionPath][docId];
1254
+ public removeDoc = async (
1255
+ collectionPath: string,
1256
+ docId: string
1257
+ ): Promise<actionResponse> => {
1258
+ const canDelete = await this.permissionCheck("delete", collectionPath);
1259
+ if (canDelete) {
1260
+ if (Object.prototype.hasOwnProperty.call(this.data, collectionPath)) {
1261
+ if (
1262
+ Object.prototype.hasOwnProperty.call(this.data[collectionPath], docId)
1263
+ ) {
1264
+ delete this.data[collectionPath][docId];
1265
+ }
473
1266
  }
1267
+ deleteDoc(doc(this.db, collectionPath, docId));
1268
+ return this.sendResponse({
1269
+ success: true,
1270
+ message: ""
1271
+ });
1272
+ } else {
1273
+ return this.sendResponse({
1274
+ success: false,
1275
+ message: `You do not have permission to delete from "${collectionPath}"`
1276
+ });
474
1277
  }
475
- deleteDoc(doc(this.db, collectionPath, docId));
476
1278
  };
477
1279
 
478
1280
  // Composable to stop snapshot listener