backend-manager 2.3.19 → 2.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "2.3.19",
3
+ "version": "2.4.1",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -27,11 +27,11 @@
27
27
  },
28
28
  "homepage": "https://itwcreativeworks.com",
29
29
  "dependencies": {
30
- "@firebase/rules-unit-testing": "^2.0.3",
30
+ "@firebase/rules-unit-testing": "^2.0.4",
31
31
  "@google-cloud/storage": "^5.20.5",
32
32
  "@sendgrid/mail": "^7.7.0",
33
33
  "@sentry/node": "^6.19.7",
34
- "backend-assistant": "^0.0.67",
34
+ "backend-assistant": "^0.0.71",
35
35
  "busboy": "^1.6.0",
36
36
  "chalk": "^4.1.2",
37
37
  "cors": "^2.8.5",
@@ -42,6 +42,7 @@
42
42
  "hcaptcha": "^0.1.1",
43
43
  "inquirer": "^8.2.4",
44
44
  "json5": "^2.2.1",
45
+ "jwt-decode": "^3.1.2",
45
46
  "lodash": "^4.17.21",
46
47
  "lowdb": "^1.0.0",
47
48
  "mailchimp-api-v3": "^1.15.0",
@@ -66,4 +67,4 @@
66
67
  "src/",
67
68
  "templates/"
68
69
  ]
69
- }
70
+ }
@@ -0,0 +1,53 @@
1
+ const decode = require('jwt-decode')
2
+
3
+ function OAuth2() {
4
+ const self = this;
5
+ self.service = 'google';
6
+ self.name = 'Google';
7
+ self.urls = {
8
+ authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
9
+ tokenize: 'https://oauth2.googleapis.com/token',
10
+ }
11
+ }
12
+
13
+ OAuth2.prototype.buildUrl = function (state, url) {
14
+ const self = this;
15
+
16
+ return new Promise(function(resolve, reject) {
17
+ if (state === 'authorize') {
18
+ // do something with url
19
+ return resolve()
20
+ } else {
21
+ return resolve()
22
+ }
23
+ });
24
+ };
25
+
26
+ OAuth2.prototype.verifyIdentity = function (tokenizeResult) {
27
+ const self = this;
28
+ const Manager = self.Manager;
29
+
30
+ return new Promise(function(resolve, reject) {
31
+ const decoded = decode(tokenizeResult.id_token);
32
+
33
+ // console.log('---decoded', decoded);
34
+
35
+ // Check if exists
36
+ Manager.libraries.admin.firestore().collection(`users`)
37
+ .where(`oauth2.${self.service}.identity.email`, '==', decoded.email)
38
+ .get()
39
+ .then(async (snap) => {
40
+ if (snap.size === 0) {
41
+ return resolve(decoded);
42
+ } else {
43
+ return reject(new Error(`This ${self.name} account is already connected to a ${Manager.config.brand.name} account`));
44
+ }
45
+ })
46
+ .catch((e) => {
47
+ return reject(e);
48
+ });
49
+
50
+ });
51
+ };
52
+
53
+ module.exports = OAuth2;
@@ -1,9 +1,18 @@
1
1
  const _ = require('lodash')
2
+ const fetch = require('wonderful-fetch');
2
3
 
3
4
  function Module() {
4
5
 
5
6
  }
6
7
 
8
+ /*
9
+ authorize: redirect or send back the URL for authorization, which will go to UJ page that sends the data back to bm_api
10
+ - if no client_id is provided, fetch from ITW/APP
11
+ tokenize: save the credentials in firestore and redirect or respond with URL to the desired end page
12
+ deauthorize: delete from firestore
13
+ refresh: call refresh on token
14
+ */
15
+
7
16
  Module.prototype.main = function () {
8
17
  const self = this;
9
18
  const Manager = self.Manager;
@@ -14,9 +23,97 @@ Module.prototype.main = function () {
14
23
  return new Promise(async function(resolve, reject) {
15
24
  self.Api.resolveUser({adminRequired: true})
16
25
  .then(async (user) => {
17
- // return resolve({data: {success: true}});
18
- // return reject(assistant.errorManager(`Failed to delete user: ${e}`, {code: 400, sentry: false, send: false, log: false}).error)
19
- console.log('---payload', payload);
26
+
27
+ self.ultimateJekyllOAuth2Url = assistant.meta.environment === 'development'
28
+ ? `http://localhost:4000/oauth2`
29
+ : `${Manager.config.brand.url}/oauth2`
30
+ self.oauth2 = null;
31
+ self.omittedPayloadFields = ['redirect', 'referrer', 'service', 'state'];
32
+
33
+ // self.ultimateJekyllOAuth2Url = `${Manager.config.brand.url}/oauth2`;
34
+
35
+ // Options
36
+ // payload.data.payload.uid = payload.data.payload.uid;
37
+ payload.data.payload.redirect = typeof payload.data.payload.redirect === 'undefined'
38
+ ? true
39
+ : payload.data.payload.redirect
40
+
41
+ payload.data.payload.referrer = typeof payload.data.payload.referrer === 'undefined'
42
+ ? (assistant.meta.environment === 'development' ? `http://localhost:4000/oauth2` : `${Manager.config.brand.url}/oauth2`)
43
+ : payload.data.payload.referrer
44
+
45
+ payload.data.payload.service = payload.data.payload.service || '';
46
+ payload.data.payload.state = payload.data.payload.state || 'authorize'; // authorize, tokenize, deauthorize, refresh, get
47
+ payload.data.payload.redirect_uri = payload.data.payload.redirect_uri
48
+ ? payload.data.payload.redirect_uri
49
+ : `${Manager.config.brand.url}/authentication/account`;
50
+
51
+ // payload.data.payload.parameters = payload.data.payload.parameters || {}
52
+
53
+ // payload.data.payload.client_id = payload.data.payload.client_id;
54
+ // payload.data.payload.scope = payload.data.payload.scope;
55
+
56
+ let newUrl;
57
+ const state = {
58
+ code: 'success',
59
+ service: payload.data.payload.service,
60
+ authenticationToken: payload.data.authenticationToken,
61
+ serverUrl: `${Manager.project.functionsUrl}/bm_api`,
62
+ referrer: payload.data.payload.referrer,
63
+ redirectUrl: payload.data.payload.redirect_uri,
64
+ }
65
+
66
+ assistant.log('OAuth2 payload', payload.data.payload);
67
+
68
+ try {
69
+ self.oauth2 = new (require(`./oauth2/${payload.data.payload.service}.js`))();
70
+ self.oauth2.parent = self;
71
+ self.oauth2.Manager = self.Manager;
72
+
73
+ newUrl = self.oauth2.urls[payload.data.payload.state]
74
+
75
+ // Set parameters
76
+ if (newUrl) {
77
+ newUrl = new URL(newUrl)
78
+
79
+ if (payload.data.payload.state === 'authorize') {
80
+ newUrl.searchParams.set('state', JSON.stringify(state));
81
+ newUrl.searchParams.set('client_id', _.get(Manager.config, `oauth2.${payload.data.payload.service}.client_id`));
82
+ newUrl.searchParams.set('scope', payload.data.payload.scope);
83
+ newUrl.searchParams.set('redirect_uri', self.ultimateJekyllOAuth2Url);
84
+
85
+ newUrl.searchParams.set('access_type', typeof payload.data.payload.access_type === 'undefined' ? 'offline' : payload.data.payload.access_type)
86
+ newUrl.searchParams.set('include_granted_scopes', typeof payload.data.payload.include_granted_scopes === 'undefined' ? 'true' : payload.data.payload.include_granted_scopes)
87
+ newUrl.searchParams.set('response_type', typeof payload.data.payload.response_type === 'undefined' ? 'code' : payload.data.payload.response_type)
88
+ }
89
+
90
+ await self.oauth2.buildUrl(payload.data.payload.state, newUrl)
91
+ .then(url => {
92
+ if (url) {
93
+ newUrl = url;
94
+ }
95
+ })
96
+ .catch(e => { throw e; });
97
+ }
98
+
99
+ } catch (e) {
100
+ return reject(e);
101
+ }
102
+
103
+ // Process by state
104
+ if (payload.data.payload.state === 'authorize') {
105
+ self.processState_authorize(newUrl)
106
+ .then(r => {resolve(r)})
107
+ .catch(e => {reject(e)})
108
+ } else if (payload.data.payload.state === 'tokenize') {
109
+ self.processState_tokenize(newUrl)
110
+ .then(r => {resolve(r)})
111
+ .catch(e => {reject(e)})
112
+ } else if (payload.data.payload.state === 'deauthorize') {
113
+ self.processState_deauthorize(newUrl)
114
+ .then(r => {resolve(r)})
115
+ .catch(e => {reject(e)})
116
+ }
20
117
  })
21
118
  .catch(e => {
22
119
  return reject(e);
@@ -25,5 +122,160 @@ Module.prototype.main = function () {
25
122
 
26
123
  };
27
124
 
125
+ Module.prototype.processState_authorize = function (newUrl) {
126
+ const self = this;
127
+ const Manager = self.Manager;
128
+ const Api = self.Api;
129
+ const assistant = self.assistant;
130
+ const payload = self.payload;
131
+
132
+ return new Promise(async function(resolve, reject) {
133
+ const finalUrl = newUrl.toString();
134
+
135
+ return resolve({
136
+ data: {
137
+ authorizationUrl: finalUrl,
138
+ },
139
+ redirect: payload.data.payload.redirect ? finalUrl : null
140
+ });
141
+ });
142
+ };
143
+
144
+ Module.prototype.processState_tokenize = function (newUrl) {
145
+ const self = this;
146
+ const Manager = self.Manager;
147
+ const Api = self.Api;
148
+ const assistant = self.assistant;
149
+ const payload = self.payload;
150
+
151
+ return new Promise(async function(resolve, reject) {
152
+ const finalUrl = newUrl.toString();
153
+
154
+ const body = {
155
+ client_id: _.get(Manager.config, `oauth2.${payload.data.payload.service}.client_id`),
156
+ client_secret: _.get(Manager.config, `oauth2.${payload.data.payload.service}.client_secret`),
157
+ grant_type: 'authorization_code',
158
+ redirect_uri: self.ultimateJekyllOAuth2Url,
159
+ code: payload.data.payload.code,
160
+ // scope: '',
161
+ };
162
+
163
+ // console.log('----body', body);
164
+
165
+ const tokenizeResponse = await fetch(finalUrl, {
166
+ method: 'POST',
167
+ timeout: 60000,
168
+ response: 'json',
169
+ tries: 2,
170
+ log: true,
171
+ body: new URLSearchParams(body),
172
+ cacheBreaker: false,
173
+ headers: {
174
+ 'Content-Type': 'application/x-www-form-urlencoded',
175
+ },
176
+ })
177
+ .then(json => json)
178
+ .catch(e => e)
179
+
180
+ // console.log('---tokenizeResponse', tokenizeResponse);
181
+
182
+ if (tokenizeResponse instanceof Error) {
183
+ return reject(tokenizeResponse);
184
+ }
185
+
186
+ // Determine identity
187
+ const verifiedIdentity = await self.oauth2.verifyIdentity(tokenizeResponse)
188
+ .then(identity => identity)
189
+ .catch(e => e);
190
+
191
+ // console.log('---verifiedIdentity', verifiedIdentity);
192
+
193
+ if (verifiedIdentity instanceof Error) {
194
+ return reject(verifiedIdentity);
195
+ }
196
+
197
+ const storeResponse = await self.libraries.admin.firestore().doc(`users/${payload.user.auth.uid}`)
198
+ .set({
199
+ oauth2: {
200
+ [payload.data.payload.service]: {
201
+ code: _.omit(
202
+ _.merge({}, payload.data.payload),
203
+ self.omittedPayloadFields,
204
+ ),
205
+ token: tokenizeResponse,
206
+ identity: verifiedIdentity,
207
+ updated: {
208
+ timestamp: assistant.meta.startTime.timestamp,
209
+ timestampUNIX: assistant.meta.startTime.timestampUNIX,
210
+ }
211
+ }
212
+ }
213
+ }, { merge: true })
214
+ .then(r => r)
215
+ .catch(e => e)
216
+
217
+ // console.log('---storeResponse', storeResponse);
218
+
219
+ if (storeResponse instanceof Error) {
220
+ return reject(storeResponse);
221
+ }
222
+
223
+ return resolve({
224
+ data: {success: true}
225
+ })
226
+
227
+ });
228
+ };
229
+
230
+ Module.prototype.processState_deauthorize = function () {
231
+ const self = this;
232
+ const Manager = self.Manager;
233
+ const Api = self.Api;
234
+ const assistant = self.assistant;
235
+ const payload = self.payload;
236
+
237
+ return new Promise(async function(resolve, reject) {
238
+ self.libraries.admin.firestore().doc(`users/${payload.user.auth.uid}`)
239
+ .set({
240
+ oauth2: {
241
+ [payload.data.payload.service]: {},
242
+ updated: {
243
+ timestamp: assistant.meta.startTime.timestamp,
244
+ timestampUNIX: assistant.meta.startTime.timestampUNIX,
245
+ }
246
+ }
247
+ }, { merge: true })
248
+ .then(function(data) {
249
+ return resolve({
250
+ data: {success: true},
251
+ });
252
+ })
253
+ .catch(function(e) {
254
+ return reject(e);
255
+ })
256
+ });
257
+ };
258
+
259
+
260
+
261
+
262
+ Module.prototype.processState_template = function (newUrl) {
263
+ const self = this;
264
+ const Manager = self.Manager;
265
+ const Api = self.Api;
266
+ const assistant = self.assistant;
267
+ const payload = self.payload;
268
+
269
+ return new Promise(async function(resolve, reject) {
270
+ const finalUrl = newUrl.toString();
271
+
272
+ return resolve({
273
+ data: {
274
+ authorizationUrl: finalUrl,
275
+ },
276
+ redirect: payload.data.payload.redirect ? finalUrl : null
277
+ });
278
+ });
279
+ };
28
280
 
29
281
  module.exports = Module;
@@ -33,6 +33,7 @@ Module.prototype.init = function (Manager, data) {
33
33
 
34
34
  Module.prototype.main = function() {
35
35
  const self = this;
36
+ const Manager = self.Manager;
36
37
  const libraries = self.libraries;
37
38
  const assistant = self.assistant;
38
39
  const req = self.req;
@@ -45,6 +46,7 @@ Module.prototype.main = function() {
45
46
  const resolved = self.resolveCommand(self.payload.data.command);
46
47
 
47
48
  self.assistant.log(`Executing: ${resolved.command}`, self.payload, JSON.stringify(self.payload), {environment: 'production'})
49
+ self.assistant.log(`Resolved URL: ${Manager.project.functionsUrl}?command=${encodeURIComponent(resolved.command)}&payload=${encodeURIComponent(JSON.stringify(self.assistant.request.data.payload))}`, {environment: 'development'})
48
50
 
49
51
  if (!resolved.exists) {
50
52
  self.payload.response.status = 400;
@@ -61,6 +63,7 @@ Module.prototype.main = function() {
61
63
  // console.log('---self.payload.response.data', self.payload.response.data);
62
64
  self.payload.response.status = result.status || self.payload.response.status || 200;
63
65
  self.payload.response.data = result.data || self.payload.response.data || {};
66
+ self.payload.response.redirect = result.redirect || self.payload.response.redirect || null;
64
67
  })
65
68
  .catch(e => {
66
69
  self.payload.response.status = e.code || 500;
@@ -77,13 +80,22 @@ Module.prototype.main = function() {
77
80
  })
78
81
  }
79
82
 
80
- if (self.payload.response.status === 200) {
81
- self.assistant.log(`Finished: ${resolved.command}`, self.payload, JSON.stringify(self.payload), {environment: 'production'})
82
- return res.status(self.payload.response.status).json(self.payload.response.data);
83
+ self.payload.response.status = _fixStatus(self.payload.response.status);
84
+
85
+ res.status(self.payload.response.status)
86
+
87
+ if (self.payload.response.status >= 200 && self.payload.response.status < 300) {
88
+ self.assistant.log(`Finished ${resolved.command} (status=${self.payload.response.status})`, self.payload, JSON.stringify(self.payload), {environment: 'production'})
89
+
90
+ if (self.payload.response.redirect) {
91
+ return res.redirect(self.payload.response.redirect);
92
+ } else {
93
+ return res.json(self.payload.response.data);
94
+ }
83
95
  } else {
84
- console.error(`Error executing ${resolved.command} @ ${resolved.path}`, self.payload.response.error)
85
- // return res.status(self.payload.response.status).send(self.payload.response.error.message);
86
- return res.status(self.payload.response.status).send(`${self.payload.response.error}`);
96
+ console.error(`Error executing ${resolved.command} @ ${resolved.path} (status=${self.payload.response.status}):`, self.payload.response.error)
97
+ // return res.send(self.payload.response.error.message);
98
+ return res.send(`${self.payload.response.error}`);
87
99
  }
88
100
  });
89
101
  }
@@ -252,4 +264,16 @@ Module.prototype.resolveUser = function (options) {
252
264
  });
253
265
  };
254
266
 
267
+ function _fixStatus(status) {
268
+ if (typeof status === 'number') {
269
+ return status;
270
+ } else {
271
+ if (status === 'ok') {
272
+ return 200
273
+ } else {
274
+ return 500
275
+ }
276
+ }
277
+ }
278
+
255
279
  module.exports = Module;
@@ -19,6 +19,8 @@ function Manager(exporter, options) {
19
19
  storage: {},
20
20
  };
21
21
 
22
+ // self.isDevelopment = false;
23
+
22
24
  return self;
23
25
  }
24
26
 
@@ -32,15 +34,16 @@ Manager.prototype.init = function (exporter, options) {
32
34
  // Set options defaults
33
35
  options = options || {};
34
36
  options.initialize = typeof options.initialize === 'undefined' ? true : options.initialize;
37
+ options.log = typeof options.log === 'undefined' ? false : options.log;
35
38
  options.setupFunctions = typeof options.setupFunctions === 'undefined' ? true : options.setupFunctions;
36
39
  options.setupFunctionsLegacy = typeof options.setupFunctionsLegacy === 'undefined' ? true : options.setupFunctionsLegacy;
37
40
  options.initializeLocalStorage = typeof options.initializeLocalStorage === 'undefined' ? false : options.initializeLocalStorage;
41
+ options.resourceZone = typeof options.resourceZone === 'undefined' ? 'us-central1' : options.resourceZone;
38
42
  options.sentry = typeof options.sentry === 'undefined' ? true : options.sentry;
39
43
  options.reportErrorsInDev = typeof options.reportErrorsInDev === 'undefined' ? false : options.reportErrorsInDev;
40
44
  options.firebaseConfig = options.firebaseConfig;
41
45
  options.useFirebaseLogger = typeof options.useFirebaseLogger === 'undefined' ? true : options.useFirebaseLogger;
42
- // options.serviceAccountPath = typeof options.serviceAccountPath === 'undefined' ? 'service-account.json' : options.serviceAccountPath;
43
- options.serviceAccountPath = typeof options.serviceAccountPath === 'undefined' ? undefined : options.serviceAccountPath;
46
+ options.serviceAccountPath = typeof options.serviceAccountPath === 'undefined' ? 'service-account.json' : options.serviceAccountPath;
44
47
  options.uniqueAppName = options.uniqueAppName || undefined;
45
48
  options.assistant = options.assistant || {};
46
49
  // options.assistant.optionsLogString = options.assistant.optionsLogString || undefined;
@@ -70,7 +73,9 @@ Manager.prototype.init = function (exporter, options) {
70
73
 
71
74
  // Set properties
72
75
  self.options = options;
73
- self.project = options.firebaseConfig || JSON.parse(process.env.FIREBASE_CONFIG);
76
+ self.project = options.firebaseConfig || JSON.parse(process.env.FIREBASE_CONFIG || '{}');
77
+ self.project.resourceZone = options.resourceZone;
78
+
74
79
  self.cwd = process.cwd();
75
80
  self.package = resolveProjectPackage();
76
81
  self.config = merge(
@@ -78,15 +83,26 @@ Manager.prototype.init = function (exporter, options) {
78
83
  self.libraries.functions.config()
79
84
  );
80
85
 
86
+ // Init assistant
81
87
  self.assistant = self.Assistant().init(undefined, options.assistant);
82
88
 
83
89
  process.env.ENVIRONMENT = !process.env.ENVIRONMENT ? self.assistant.meta.environment : process.env.ENVIRONMENT;
84
90
 
91
+ // set more properties (need to wait for assistant to determine if DEV)
92
+ self.project.functionsUrl = self.assistant.meta.environment === 'development'
93
+ ? `http://localhost:5001/${self.project.projectId}/${self.project.resourceZone}`
94
+ : `https://${self.project.resourceZone}-${self.project.projectId}.cloudfunctions.net`;
95
+
96
+
85
97
  // Use the working Firebase logger that they disabled for whatever reason
86
98
  if (self.assistant.meta.environment !== 'development' && options.useFirebaseLogger) {
87
99
  require('firebase-functions/lib/logger/compat');
88
100
  }
89
101
 
102
+ if (options.log) {
103
+ self.assistant.log('process.env', process.env, {environment: 'production'})
104
+ }
105
+
90
106
  // Setup sentry
91
107
  if (self.options.sentry) {
92
108
  const sentryRelease = `${get(self.config, 'app.id') || self.project.projectId}@${self.package.version}`;
@@ -114,16 +130,18 @@ Manager.prototype.init = function (exporter, options) {
114
130
  // Setup options features
115
131
  if (self.options.initialize) {
116
132
  // console.log('Initializing:', self.project);
133
+ // console.log('----process.env.GOOGLE_APPLICATION_CREDENTIALS', process.env.GOOGLE_APPLICATION_CREDENTIALS);
117
134
  try {
118
- if (options.serviceAccountPath && options.uniqueAppName) {
135
+ // console.log('---process.env.GOOGLE_APPLICATION_CREDENTIALS', process.env.GOOGLE_APPLICATION_CREDENTIALS);
136
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
137
+ self.libraries.initializedAdmin = self.libraries.admin.initializeApp();
138
+ } else {
119
139
  self.libraries.initializedAdmin = self.libraries.admin.initializeApp({
120
140
  credential: self.libraries.admin.credential.cert(
121
141
  require(path.resolve(self.cwd, options.serviceAccountPath))
122
142
  ),
123
143
  databaseURL: self.project.databaseURL,
124
144
  }, options.uniqueAppName);
125
- } else {
126
- self.libraries.initializedAdmin = self.libraries.admin.initializeApp();
127
145
  }
128
146
  } catch (e) {
129
147
  console.error('Failed to call .initializeApp()', e);