backend-manager 4.1.2 → 4.2.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.
@@ -69,7 +69,7 @@ function tryUrl(self) {
69
69
  : `${protocol}://${host}/${self.meta.name}`;
70
70
  }
71
71
  } else if (projectType === 'custom') {
72
- return `@@@TODO`;
72
+ return `@TODO`;
73
73
  }
74
74
 
75
75
  return '';
@@ -598,22 +598,44 @@ function _attachHeaderProperties(self, options, error) {
598
598
  BackendAssistant.prototype.authenticate = async function (options) {
599
599
  const self = this;
600
600
 
601
- let admin = self.ref.admin;
602
- let functions = self.ref.functions;
603
- let req = self.ref.req;
604
- let res = self.ref.res;
605
- let data = self.request.data;
601
+ // Shortcuts
602
+ const admin = self.ref.admin;
603
+ const functions = self.ref.functions;
604
+ const req = self.ref.req;
605
+ const res = self.ref.res;
606
+ const data = self.request.data;
607
+
608
+ // Get stored backendManagerKey
609
+ const BACKEND_MANAGER_KEY = self.Manager?.config?.backend_manager?.key || '';
610
+
611
+ // Build the ID token from the request
606
612
  let idToken;
613
+ let backendManagerKey;
614
+ // let user;
607
615
 
616
+ // Set options
608
617
  options = options || {};
609
618
  options.resolve = typeof options.resolve === 'undefined' ? true : options.resolve;
619
+ options.debug = typeof options.debug === 'undefined' ? false : options.debug;
610
620
 
611
621
  function _resolve(user) {
622
+ // Resolve the properties
612
623
  user = user || {};
613
624
  user.authenticated = typeof user.authenticated === 'undefined'
614
625
  ? false
615
626
  : user.authenticated;
616
627
 
628
+ // Validate BACKEND_MANAGER_KEY
629
+ if (backendManagerKey && backendManagerKey === BACKEND_MANAGER_KEY) {
630
+ // Update roles
631
+ user.roles = user.roles || {};
632
+ user.roles.admin = true;
633
+
634
+ // Set authenticated
635
+ user.authenticated = true;
636
+ }
637
+
638
+ // Resolve the user
617
639
  if (options.resolve) {
618
640
  self.request.user = self.resolveAccount(user);
619
641
  return self.request.user;
@@ -622,91 +644,102 @@ BackendAssistant.prototype.authenticate = async function (options) {
622
644
  }
623
645
  }
624
646
 
625
- if (req?.headers?.authorization && req?.headers?.authorization?.startsWith('Bearer ')) {
647
+ // Get shortcuts
648
+ const authHeader = req?.headers?.authorization || '';
649
+
650
+ // Extract the BEM token
651
+ // Having this is separate from the ID token allows for the user to be authenticated as an ADMIN
652
+ if (options.backendManagerKey || data.backendManagerKey) {
653
+ // Read token from backendManagerKey or authenticationToken or apiKey
654
+ backendManagerKey = options.backendManagerKey || data.backendManagerKey;
655
+
656
+ // Log the token
657
+ self.log('Found "backendManagerKey" parameter', backendManagerKey);
658
+ }
659
+
660
+ // Extract the token / API key
661
+ // This is the main token that will be used to authenticate the user (it can be a JWT or a user's API key)
662
+ if (authHeader.startsWith('Bearer ')) {
626
663
  // Read the ID Token from the Authorization header.
627
- idToken = req.headers.authorization.split('Bearer ')[1];
664
+ idToken = authHeader.split('Bearer ')[1];
665
+
666
+ // Log the token
628
667
  self.log('Found "Authorization" header', idToken);
629
668
  } else if (req?.cookies?.__session) {
630
669
  // Read the ID Token from cookie.
631
670
  idToken = req.cookies.__session;
671
+
672
+ // Log the token
632
673
  self.log('Found "__session" cookie', idToken);
633
- } else if (data.backendManagerKey || data.authenticationToken) {
634
- // Check with custom BEM Token
635
- let storedApiKey;
674
+ } else if (
675
+ options.authenticationToken || data.authenticationToken
676
+ || options.apiKey || data.apiKey
677
+ ) {
678
+ // Read token OR API Key from options or data
679
+ idToken = options.authenticationToken || data.authenticationToken
680
+ || options.apiKey || data.apiKey;
681
+
682
+ // Log the token
683
+ self.log('Found "authenticationToken" parameter', idToken);
684
+ } else {
685
+ // No token found
686
+ return _resolve(self.request.user);
687
+ }
688
+
689
+ // Check if the token is a JWT
690
+ if (isJWT(idToken)) {
691
+ // Check with firebase
636
692
  try {
637
- // Disabled this 5/11/24 because i dont know why we would need to do functions.config() if we already have the Manager
638
- // const workingConfig = self.Manager?.config || functions.config();
639
- storedApiKey = self.Manager?.config?.backend_manager?.key || '';
640
- } catch (e) {
641
- // Do nothing
642
- }
693
+ const decodedIdToken = await admin.auth().verifyIdToken(idToken);
643
694
 
644
- // Set idToken as working token of either backendManagerKey or authenticationToken
645
- idToken = data.backendManagerKey || data.authenticationToken;
695
+ // Log the token
696
+ if (options.debug) {
697
+ self.log('JWT token decoded', decodedIdToken.email, decodedIdToken.user_id);
698
+ }
646
699
 
647
- // Log the token
648
- self.log('Found "backendManagerKey" or "authenticationToken" parameter', {storedApiKey: storedApiKey, idToken: idToken});
700
+ // Get the user
701
+ await admin.firestore().doc(`users/${decodedIdToken.user_id}`)
702
+ .get()
703
+ .then((doc) => {
704
+ // Set the user
705
+ if (doc.exists) {
706
+ self.request.user = Object.assign({}, self.request.user, doc.data());
707
+ self.request.user.authenticated = true;
708
+ self.request.user.auth.uid = decodedIdToken.user_id;
709
+ self.request.user.auth.email = decodedIdToken.email;
710
+ }
711
+
712
+ // Log the user
713
+ if (options.debug) {
714
+ self.log('Found user doc', self.request.user)
715
+ }
716
+ })
649
717
 
650
- // Check if the token is correct
651
- if (storedApiKey && (storedApiKey === data.backendManagerKey || storedApiKey === data.authenticationToken)) {
652
- self.request.user.authenticated = true;
653
- self.request.user.roles.admin = true;
718
+ // Return the user
654
719
  return _resolve(self.request.user);
655
- }
656
- } else if (options.apiKey || data.apiKey) {
657
- const apiKey = options.apiKey || data.apiKey;
658
- self.log('Found "options.apiKey"', apiKey);
720
+ } catch (error) {
721
+ self.error('Error while verifying JWT:', error);
659
722
 
660
- if (apiKey.includes('test')) {
723
+ // Return the user
661
724
  return _resolve(self.request.user);
662
725
  }
663
-
726
+ } else {
727
+ // Query by API key
664
728
  await admin.firestore().collection(`users`)
665
- .where('api.privateKey', '==', apiKey)
729
+ .where('api.privateKey', '==', idToken)
666
730
  .get()
667
- .then(function(querySnapshot) {
668
- querySnapshot.forEach(function(doc) {
731
+ .then((querySnapshot) => {
732
+ querySnapshot.forEach((doc) => {
669
733
  self.request.user = doc.data();
734
+ self.request.user = Object.assign({}, self.request.user, doc.data());
670
735
  self.request.user.authenticated = true;
671
736
  });
672
737
  })
673
- .catch(function(error) {
738
+ .catch((error) => {
674
739
  console.error('Error getting documents: ', error);
675
740
  });
676
741
 
677
- return _resolve(self.request.user);
678
- } else {
679
- // self.log('No Firebase ID token was able to be extracted.',
680
- // 'Make sure you authenticate your request by providing either the following HTTP header:',
681
- // 'Authorization: Bearer <Firebase ID Token>',
682
- // 'or by passing a "__session" cookie',
683
- // 'or by passing backendManagerKey or authenticationToken in the body or query');
684
-
685
- return _resolve(self.request.user);
686
- }
687
-
688
- // Check with firebase
689
- try {
690
- const decodedIdToken = await admin.auth().verifyIdToken(idToken);
691
- if (options.debug) {
692
- self.log('Token correctly decoded', decodedIdToken.email, decodedIdToken.user_id);
693
- }
694
- await admin.firestore().doc(`users/${decodedIdToken.user_id}`)
695
- .get()
696
- .then(async function (doc) {
697
- if (doc.exists) {
698
- self.request.user = Object.assign({}, self.request.user, doc.data());
699
- }
700
- self.request.user.authenticated = true;
701
- self.request.user.auth.uid = decodedIdToken.user_id;
702
- self.request.user.auth.email = decodedIdToken.email;
703
- if (options.debug) {
704
- self.log('Found user doc', self.request.user)
705
- }
706
- })
707
- return _resolve(self.request.user);
708
- } catch (error) {
709
- self.error('Error while verifying Firebase ID token:', error);
742
+ // Return the user
710
743
  return _resolve(self.request.user);
711
744
  }
712
745
  };
@@ -726,7 +759,7 @@ BackendAssistant.prototype.parseRepo = function (repo) {
726
759
  }
727
760
 
728
761
  // Remove unnecessary parts
729
- repoSplit = repoSplit.filter(function(value, index, arr){
762
+ repoSplit = repoSplit.filter((value, index, arr) => {
730
763
  return value !== 'http:'
731
764
  && value !== 'https:'
732
765
  && value !== ''
@@ -1034,6 +1067,21 @@ BackendAssistant.prototype.parseMultipartFormData = function (options) {
1034
1067
  });
1035
1068
  }
1036
1069
 
1070
+ const isJWT = (token) => {
1071
+ // Ensure the token has three parts separated by dots
1072
+ const parts = token.split('.');
1073
+
1074
+ try {
1075
+ // Decode the header (first part) to verify it is JSON
1076
+ const header = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf8'));
1077
+ // Check for expected JWT keys in the header
1078
+ return header.alg && header.typ === 'JWT';
1079
+ } catch (err) {
1080
+ // If parsing fails, it's not a valid JWT
1081
+ return false;
1082
+ }
1083
+ };
1084
+
1037
1085
  // Not sure what this is for? But it has a good serializer code
1038
1086
  // Disabled 2024-03-21 because there was another stringify() function that i was intending to use but it was actually using this
1039
1087
  // It was adding escaped quotes to strings
@@ -62,7 +62,7 @@ Settings.prototype.resolve = function (assistant, schema, settings, options) {
62
62
  // console.log('---self.settings', self.settings);
63
63
 
64
64
  // Iterate each key and check for some things
65
- processSchema(schema, (path, schemaNode) => {
65
+ iterateSchema(schema, (path, schemaNode) => {
66
66
  const originalValue = _.get(settings, path);
67
67
  const resolvedValue = _.get(self.settings, path);
68
68
  let replaceValue = undefined;
@@ -154,7 +154,7 @@ Settings.prototype.constant = function (name, options) {
154
154
  }
155
155
  };
156
156
 
157
- function processSchema(schema, fn, path) {
157
+ function iterateSchema(schema, fn, path) {
158
158
  path = path || '';
159
159
 
160
160
  // Base case: Check if the current level has 'types' and 'default', indicating metadata
@@ -167,7 +167,7 @@ function processSchema(schema, fn, path) {
167
167
  // Recursive case: Iterate through nested keys if we're not at a metadata node
168
168
  Object.keys(schema).forEach(key => {
169
169
  const nextPath = path ? `${path}.${key}` : key;
170
- processSchema(schema[key], fn, nextPath);
170
+ iterateSchema(schema[key], fn, nextPath);
171
171
  });
172
172
  }
173
173
 
@@ -871,11 +871,11 @@ Manager.prototype.setupFunctions = function (exporter, options) {
871
871
  .auth.user()
872
872
  .onDelete(async (user, context) => self._process((new (require(`${core}/events/auth/on-delete.js`))()).init(self, { user: user, context: context})));
873
873
 
874
- exporter.bm_subOnWrite =
874
+ exporter.bm_notificationsOnWrite =
875
875
  self.libraries.functions
876
876
  .runWith({memory: '256MB', timeoutSeconds: 60})
877
- .firestore.document('notifications/subscriptions/all/{token}')
878
- .onWrite(async (change, context) => self._process((new (require(`${core}/events/firestore/on-subscription.js`))()).init(self, { change: change, context: context, })));
877
+ .firestore.document('notifications/{token}')
878
+ .onWrite(async (change, context) => self._process((new (require(`${core}/events/firestore/notifications/on-write.js`))()).init(self, { change: change, context: context, })));
879
879
 
880
880
  // Setup cron jobs
881
881
  exporter.bm_cronDaily =
@@ -126,24 +126,24 @@ describe("BackendManager Tests", () => {
126
126
  describe("notifications", () => {
127
127
  it("unauthenticated can subscribe", async () => {
128
128
  const db = auth(accounts.unauthenticated);
129
- const doc = db.doc(`notifications/subscriptions/all/token`);
129
+ const doc = db.doc(`notifications/token`);
130
130
  await firebase.assertSucceeds(doc.set({token: 'token'}));
131
131
  });
132
132
  it("authenticated can subscribe", async () => {
133
133
  const db = auth(accounts.regular);
134
- const doc = db.doc(`notifications/subscriptions/all/token`);
134
+ const doc = db.doc(`notifications/token`);
135
135
  await firebase.assertSucceeds(doc.set({token: 'token'}));
136
136
  });
137
137
 
138
138
  it("unauthenticated can read subscription by token", async () => {
139
139
  const db = auth(accounts.unauthenticated);
140
- const doc = db.doc(`notifications/subscriptions/all/token`);
140
+ const doc = db.doc(`notifications/token`);
141
141
  await firebase.assertSucceeds(doc.get());
142
142
  });
143
143
 
144
144
  it("authenticated can read subscription by token", async () => {
145
145
  const db = auth(accounts.regular);
146
- const doc = db.doc(`notifications/subscriptions/all/token`);
146
+ const doc = db.doc(`notifications/token`);
147
147
  await firebase.assertSucceeds(doc.get());
148
148
  });
149
149
 
@@ -9,7 +9,7 @@ service cloud.firestore {
9
9
  match /{document=**} {
10
10
  allow read, write: if isAdmin();
11
11
  }
12
-
12
+
13
13
  // Protect user account data
14
14
  match /users/{uid} {
15
15
  allow read: if belongsTo(uid);
@@ -17,8 +17,8 @@ service cloud.firestore {
17
17
  }
18
18
 
19
19
  // Protect notification data
20
- match /notifications/subscriptions/all/{token} {
21
- allow read: if existingData().token == token || belongsTo(existingData().link.user.pk);
20
+ match /notifications/{token} {
21
+ allow read: if existingData().token == token || belongsTo(existingData().owner.uid);
22
22
  allow update: if existingData().token == token;
23
23
  allow create: if true;
24
24
  }
@@ -40,7 +40,7 @@ service cloud.firestore {
40
40
  }
41
41
  function belongsTo(identity) {
42
42
  return isAuthenticated() && (authUid() == identity || authEmail() == identity);
43
- // eventually include a check for (existingData().link.email == identity)...(in case its a doc owned by a user that's not actually user doc)
43
+ // eventually include a check for (existingData().owner.uid == identity)...(in case its a doc owned by a user that's not actually user doc)
44
44
  }
45
45
 
46
46
  function getRoles() {