backend-manager 2.5.124 → 3.0.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/CHANGELOG.md +76 -0
- package/package.json +13 -10
- package/src/cli/cli.js +42 -54
- package/src/manager/functions/core/actions/api/general/send-email.js +5 -5
- package/src/manager/functions/core/actions/api/special/setup-electron-manager-client.js +2 -2
- package/src/manager/functions/core/actions/api/user/sign-up.js +2 -16
- package/src/manager/functions/core/events/auth/before-create.js +2 -0
- package/src/manager/functions/core/events/auth/before-signin.js +2 -0
- package/src/manager/helpers/analytics.js +3 -3
- package/src/manager/helpers/api-manager.js +3 -3
- package/src/manager/helpers/assistant.js +733 -0
- package/src/manager/helpers/user.js +7 -3
- package/src/manager/index.js +18 -15
- package/src/cli/config.json +0 -3
- package/src/manager/index-bu.js +0 -235
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const uuid = require('uuid');
|
|
5
|
+
let JSON5;
|
|
6
|
+
|
|
7
|
+
function BackendAssistant() {
|
|
8
|
+
this.meta = {};
|
|
9
|
+
this.initialized = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tryParse(input) {
|
|
13
|
+
var ret;
|
|
14
|
+
|
|
15
|
+
JSON5 = JSON5 || require('json5');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
ret = JSON5.parse(input);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
ret = input
|
|
21
|
+
}
|
|
22
|
+
return ret;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
BackendAssistant.prototype.init = function (ref, options) {
|
|
26
|
+
const self = this;
|
|
27
|
+
|
|
28
|
+
options = options || {};
|
|
29
|
+
options.accept = options.accept || 'json';
|
|
30
|
+
options.showOptionsLog = typeof options.showOptionsLog !== 'undefined' ? options.showOptionsLog : false;
|
|
31
|
+
options.optionsLogString = typeof options.optionsLogString !== 'undefined' ? options.optionsLogString : '\n\n\n\n\n';
|
|
32
|
+
options.fileSavePath = options.fileSavePath || process.env.npm_package_name || '';
|
|
33
|
+
|
|
34
|
+
const now = new Date();
|
|
35
|
+
|
|
36
|
+
self.meta = {};
|
|
37
|
+
|
|
38
|
+
self.meta.startTime = {};
|
|
39
|
+
self.meta.startTime.timestamp = now.toISOString();
|
|
40
|
+
self.meta.startTime.timestampUNIX = Math.round((now.getTime()) / 1000);
|
|
41
|
+
|
|
42
|
+
self.meta.name = options.functionName || process.env.FUNCTION_TARGET || 'unnamed';
|
|
43
|
+
self.meta.environment = options.environment || self.getEnvironment();
|
|
44
|
+
self.meta.type = options.functionType || process.env.FUNCTION_SIGNATURE_TYPE || 'unknown';
|
|
45
|
+
|
|
46
|
+
self.ref = {};
|
|
47
|
+
ref = ref || {};
|
|
48
|
+
self.ref.res = ref.res || {};
|
|
49
|
+
self.ref.req = ref.req || {};
|
|
50
|
+
self.ref.admin = ref.admin || {};
|
|
51
|
+
self.ref.functions = ref.functions || {};
|
|
52
|
+
self.ref.Manager = ref.Manager || {};
|
|
53
|
+
|
|
54
|
+
// Set ID
|
|
55
|
+
try {
|
|
56
|
+
self.id = self.ref.Manager.Utilities().randomId();
|
|
57
|
+
} catch {
|
|
58
|
+
self.id = now.getTime();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Set stuff about request
|
|
62
|
+
self.request = {};
|
|
63
|
+
self.request.referrer = (self.ref.req.headers || {}).referrer || (self.ref.req.headers || {}).referer || '';
|
|
64
|
+
self.request.method = (self.ref.req.method || undefined);
|
|
65
|
+
|
|
66
|
+
// Set geolocation data
|
|
67
|
+
self.request.geolocation = {
|
|
68
|
+
ip: self.getHeaderIp(self.ref.req.headers),
|
|
69
|
+
continent: self.getHeaderContinent(self.ref.req.headers),
|
|
70
|
+
country: self.getHeaderCountry(self.ref.req.headers),
|
|
71
|
+
region: self.getHeaderRegion(self.ref.req.headers),
|
|
72
|
+
city: self.getHeaderCity(self.ref.req.headers),
|
|
73
|
+
latitude: self.getHeaderLatitude(self.ref.req.headers),
|
|
74
|
+
longitude: self.getHeaderLongitude(self.ref.req.headers),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Set client data
|
|
78
|
+
self.request.client = {
|
|
79
|
+
userAgent: self.getHeaderUserAgent(self.ref.req.headers),
|
|
80
|
+
language: self.getHeaderLanguage(self.ref.req.headers),
|
|
81
|
+
platform: self.getHeaderPlatform(self.ref.req.headers),
|
|
82
|
+
mobile: self.getHeaderMobile(self.ref.req.headers),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Deprecated notice for old properties
|
|
86
|
+
Object.defineProperty(self.request, 'ip', {
|
|
87
|
+
get: function() {
|
|
88
|
+
console.error('⛔️ [Deprecation]: request.ip is deprecated, use request.geolocation.ip instead');
|
|
89
|
+
return self.request.geolocation.ip;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(self.request, 'country', {
|
|
93
|
+
get: function() {
|
|
94
|
+
console.error('⛔️ [Deprecation]: request.country is deprecated, use request.geolocation.country instead');
|
|
95
|
+
return self.request.geolocation.country;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
Object.defineProperty(self.request, 'userAgent', {
|
|
99
|
+
get: function() {
|
|
100
|
+
console.error('⛔️ [Deprecation]: request.userAgent is deprecated, use request.client.userAgent instead');
|
|
101
|
+
return self.request.client.userAgent;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
MORE HEADERS TO GET
|
|
107
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Platform-Version
|
|
108
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Model
|
|
109
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Mobile
|
|
110
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Full-Version-List
|
|
111
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Full-Version
|
|
112
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Arch
|
|
113
|
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
self.request.type = (self.ref.req.xhr || _.get(self.ref.req, 'headers.accept', '').indexOf('json') > -1) || (_.get(self.ref.req, 'headers.content-type', '').indexOf('json') > -1) ? 'ajax' : 'form';
|
|
117
|
+
self.request.path = (self.ref.req.path || '');
|
|
118
|
+
self.request.user = self.resolveAccount({authenticated: false});
|
|
119
|
+
if (options.accept === 'json') {
|
|
120
|
+
self.request.body = tryParse(self.ref.req.body || '{}');
|
|
121
|
+
self.request.query = tryParse(self.ref.req.query || '{}');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
self.request.headers = (self.ref.req.headers || {});
|
|
125
|
+
self.request.data = Object.assign(
|
|
126
|
+
{},
|
|
127
|
+
_.cloneDeep(self.request.body || {}),
|
|
128
|
+
_.cloneDeep(self.request.query || {})
|
|
129
|
+
);
|
|
130
|
+
self.request.multipartData = {
|
|
131
|
+
fields: {},
|
|
132
|
+
files: {},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Log the request
|
|
136
|
+
// if (Object.keys(self.request.data).length > 0) {
|
|
137
|
+
// self.log('Request:', self.request.data, {
|
|
138
|
+
// ip: self.request.ip,
|
|
139
|
+
|
|
140
|
+
// }, {environment: 'production'});
|
|
141
|
+
// }
|
|
142
|
+
|
|
143
|
+
// Constants
|
|
144
|
+
self.constant = {};
|
|
145
|
+
self.constant.pastTime = {};
|
|
146
|
+
self.constant.pastTime.timestamp = '1999-01-01T00:00:00Z';
|
|
147
|
+
self.constant.pastTime.timestampUNIX = 915148800;
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
(self.meta.environment === 'development')
|
|
151
|
+
&& ((self.request.method !== 'OPTIONS') || (self.request.method === 'OPTIONS' && options.showOptionsLog))
|
|
152
|
+
&& (self.request.method !== 'undefined')
|
|
153
|
+
// && (self.request.method !== 'undefined' && typeof self.request.method !== 'undefined')
|
|
154
|
+
) {
|
|
155
|
+
console.log(options.optionsLogString);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
self.tmpdir = path.resolve(os.tmpdir(), options.fileSavePath, uuid.v4());
|
|
159
|
+
|
|
160
|
+
self.initialized = true;
|
|
161
|
+
|
|
162
|
+
return self;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
BackendAssistant.prototype.getEnvironment = function () {
|
|
166
|
+
// return (process.env.FUNCTIONS_EMULATOR === true || process.env.FUNCTIONS_EMULATOR === 'true' || process.env.ENVIRONMENT !== 'production' ? 'development' : 'production')
|
|
167
|
+
if (process.env.ENVIRONMENT === 'production') {
|
|
168
|
+
return 'production';
|
|
169
|
+
} else if (
|
|
170
|
+
process.env.ENVIRONMENT === 'development'
|
|
171
|
+
|| process.env.FUNCTIONS_EMULATOR === true
|
|
172
|
+
|| process.env.FUNCTIONS_EMULATOR === 'true'
|
|
173
|
+
|| process.env.TERM_PROGRAM === 'Apple_Terminal'
|
|
174
|
+
|| process.env.TERM_PROGRAM === 'vscode'
|
|
175
|
+
) {
|
|
176
|
+
return 'development';
|
|
177
|
+
} else {
|
|
178
|
+
return 'production'
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
BackendAssistant.prototype.logProd = function () {
|
|
183
|
+
const self = this;
|
|
184
|
+
|
|
185
|
+
self._log.apply(self, args);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
BackendAssistant.prototype.log = function () {
|
|
189
|
+
const self = this;
|
|
190
|
+
|
|
191
|
+
let args = Array.prototype.slice.call(arguments);
|
|
192
|
+
let last = args[args.length - 1];
|
|
193
|
+
let override = last && typeof last === 'object' && last.environment === 'production';
|
|
194
|
+
|
|
195
|
+
if (self.meta.environment === 'development' || override) {
|
|
196
|
+
if (override) {
|
|
197
|
+
args.pop();
|
|
198
|
+
}
|
|
199
|
+
self._log.apply(self, args);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
BackendAssistant.prototype.error = function () {
|
|
204
|
+
const self = this;
|
|
205
|
+
|
|
206
|
+
let args = Array.prototype.slice.call(arguments);
|
|
207
|
+
|
|
208
|
+
args.unshift('error');
|
|
209
|
+
self.log.apply(self, args);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
BackendAssistant.prototype.warn = function () {
|
|
213
|
+
const self = this;
|
|
214
|
+
|
|
215
|
+
let args = Array.prototype.slice.call(arguments);
|
|
216
|
+
|
|
217
|
+
args.unshift('warn');
|
|
218
|
+
self.log.apply(self, args);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
BackendAssistant.prototype._log = function() {
|
|
222
|
+
const self = this;
|
|
223
|
+
|
|
224
|
+
// 1. Convert args to a normal array
|
|
225
|
+
let logs = [...Array.prototype.slice.call(arguments)];
|
|
226
|
+
|
|
227
|
+
// 2. Prepend log prefix log string
|
|
228
|
+
logs.unshift(`[${self.meta.name}/${self.id} @ ${new Date().toISOString()}]:`);
|
|
229
|
+
|
|
230
|
+
// 3. Pass along arguments to console.log
|
|
231
|
+
if (logs[1] === 'error') {
|
|
232
|
+
logs.splice(1,1)
|
|
233
|
+
console.error.apply(console, logs);
|
|
234
|
+
} else if (logs[1] === 'warn') {
|
|
235
|
+
logs.splice(1,1)
|
|
236
|
+
console.warn.apply(console, logs);
|
|
237
|
+
} else if (logs[1] === 'log') {
|
|
238
|
+
logs.splice(1,1)
|
|
239
|
+
console.log.apply(console, logs);
|
|
240
|
+
} else {
|
|
241
|
+
console.log.apply(console, logs);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
BackendAssistant.prototype.errorManager = function(e, options) {
|
|
246
|
+
const self = this;
|
|
247
|
+
|
|
248
|
+
options = options || {};
|
|
249
|
+
options.code = typeof options.code === 'undefined' ? 500 : options.code;
|
|
250
|
+
options.log = typeof options.log === 'undefined' ? true : options.log;
|
|
251
|
+
options.sentry = typeof options.sentry === 'undefined' ? true : options.sentry;
|
|
252
|
+
options.send = typeof options.send === 'undefined' ? true : options.send;
|
|
253
|
+
|
|
254
|
+
const newError = e instanceof Error ? e : new Error(e);
|
|
255
|
+
|
|
256
|
+
// Attach properties
|
|
257
|
+
Object.keys(options)
|
|
258
|
+
.forEach((item, i) => {
|
|
259
|
+
Object.assign(newError , { [item]: options[item] })
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
// Log the error
|
|
264
|
+
if (options.log) {
|
|
265
|
+
self.error(newError);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Send error to Sentry
|
|
269
|
+
if (options.sentry) {
|
|
270
|
+
self.ref.Manager.libraries.sentry.captureException(newError);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Quit and respond to the request
|
|
274
|
+
if (options.send && self.ref.res && self.ref.res.status) {
|
|
275
|
+
self.ref.res.status(options.code).send(newError ? newError.message || newError : 'Unknown error');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
error: newError,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
BackendAssistant.prototype.authenticate = async function (options) {
|
|
285
|
+
const self = this;
|
|
286
|
+
|
|
287
|
+
let admin = self.ref.admin;
|
|
288
|
+
let functions = self.ref.functions;
|
|
289
|
+
let req = self.ref.req;
|
|
290
|
+
let res = self.ref.res;
|
|
291
|
+
let data = self.request.data;
|
|
292
|
+
let idToken;
|
|
293
|
+
|
|
294
|
+
options = options || {};
|
|
295
|
+
options.resolve = typeof options.resolve === 'undefined' ? true : options.resolve;
|
|
296
|
+
|
|
297
|
+
const logOptions = {environment: options.log ? 'production' : 'development'}
|
|
298
|
+
|
|
299
|
+
function _resolve(user) {
|
|
300
|
+
user = user || {};
|
|
301
|
+
user.authenticated = typeof user.authenticated === 'undefined'
|
|
302
|
+
? false
|
|
303
|
+
: user.authenticated;
|
|
304
|
+
|
|
305
|
+
if (options.resolve) {
|
|
306
|
+
self.request.user = self.resolveAccount(user);
|
|
307
|
+
return self.request.user;
|
|
308
|
+
} else {
|
|
309
|
+
return user;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (req.headers && req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
|
|
314
|
+
// Read the ID Token from the Authorization header.
|
|
315
|
+
idToken = req.headers.authorization.split('Bearer ')[1];
|
|
316
|
+
self.log('Found "Authorization" header', idToken, logOptions);
|
|
317
|
+
} else if (req.cookies && req.cookies.__session) {
|
|
318
|
+
// Read the ID Token from cookie.
|
|
319
|
+
idToken = req.cookies.__session;
|
|
320
|
+
self.log('Found "__session" cookie', idToken, logOptions);
|
|
321
|
+
} else if (data.backendManagerKey || data.authenticationToken) {
|
|
322
|
+
// Check with custom BEM Token
|
|
323
|
+
let storedApiKey;
|
|
324
|
+
try {
|
|
325
|
+
const workingConfig = _.get(self.ref.Manager, 'config') || functions.config();
|
|
326
|
+
storedApiKey = _.get(workingConfig, 'backend_manager.key', '')
|
|
327
|
+
} catch (e) {
|
|
328
|
+
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
idToken = data.backendManagerKey || data.authenticationToken;
|
|
332
|
+
|
|
333
|
+
self.log('Found "backendManagerKey" or "authenticationToken" parameter', {storedApiKey: storedApiKey, idToken: idToken}, logOptions);
|
|
334
|
+
|
|
335
|
+
if (storedApiKey && (storedApiKey === data.backendManagerKey || storedApiKey === data.authenticationToken)) {
|
|
336
|
+
self.request.user.authenticated = true;
|
|
337
|
+
self.request.user.roles.admin = true;
|
|
338
|
+
return _resolve(self.request.user);
|
|
339
|
+
}
|
|
340
|
+
} else if (options.apiKey) {
|
|
341
|
+
self.log('Found "options.apiKey"', options.apiKey, logOptions);
|
|
342
|
+
if (options.apiKey.includes('test')) {
|
|
343
|
+
return _resolve(self.request.user);
|
|
344
|
+
}
|
|
345
|
+
await admin.firestore().collection(`users`)
|
|
346
|
+
.where('api.privateKey', '==', options.apiKey)
|
|
347
|
+
.get()
|
|
348
|
+
.then(function(querySnapshot) {
|
|
349
|
+
querySnapshot.forEach(function(doc) {
|
|
350
|
+
self.request.user = doc.data();
|
|
351
|
+
self.request.user.authenticated = true;
|
|
352
|
+
});
|
|
353
|
+
})
|
|
354
|
+
.catch(function(error) {
|
|
355
|
+
console.error('Error getting documents: ', error);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return _resolve(self.request.user);
|
|
359
|
+
} else {
|
|
360
|
+
self.log('No Firebase ID token was able to be extracted.',
|
|
361
|
+
'Make sure you authenticate your request by providing either the following HTTP header:',
|
|
362
|
+
'Authorization: Bearer <Firebase ID Token>',
|
|
363
|
+
'or by passing a "__session" cookie',
|
|
364
|
+
'or by passing backendManagerKey or authenticationToken in the body or query', logOptions);
|
|
365
|
+
|
|
366
|
+
return _resolve(self.request.user);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check with firebase
|
|
370
|
+
try {
|
|
371
|
+
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
|
|
372
|
+
if (options.debug) {
|
|
373
|
+
self.log('Token correctly decoded', decodedIdToken.email, decodedIdToken.user_id, logOptions);
|
|
374
|
+
}
|
|
375
|
+
await admin.firestore().doc(`users/${decodedIdToken.user_id}`)
|
|
376
|
+
.get()
|
|
377
|
+
.then(async function (doc) {
|
|
378
|
+
if (doc.exists) {
|
|
379
|
+
self.request.user = Object.assign({}, self.request.user, doc.data());
|
|
380
|
+
}
|
|
381
|
+
self.request.user.authenticated = true;
|
|
382
|
+
self.request.user.auth.uid = decodedIdToken.user_id;
|
|
383
|
+
self.request.user.auth.email = decodedIdToken.email;
|
|
384
|
+
if (options.debug) {
|
|
385
|
+
self.log('Found user doc', self.request.user, logOptions)
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
return _resolve(self.request.user);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
self.error('Error while verifying Firebase ID token:', error, logOptions);
|
|
391
|
+
return _resolve(self.request.user);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
BackendAssistant.prototype.resolveAccount = function (user) {
|
|
396
|
+
const ResolveAccount = new (require('resolve-account'))();
|
|
397
|
+
|
|
398
|
+
return ResolveAccount.resolve(undefined, user)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
BackendAssistant.prototype.parseRepo = function (repo) {
|
|
402
|
+
let repoSplit = repo.split('/');
|
|
403
|
+
for (var i = 0; i < repoSplit.length; i++) {
|
|
404
|
+
repoSplit[i] = repoSplit[i].replace('.git', '');
|
|
405
|
+
}
|
|
406
|
+
repoSplit = repoSplit.filter(function(value, index, arr){
|
|
407
|
+
return value !== 'http:' &&
|
|
408
|
+
value !== 'https:' &&
|
|
409
|
+
value !== '' &&
|
|
410
|
+
value !== 'github.com';
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
user: repoSplit[0],
|
|
414
|
+
name: repoSplit[1],
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
BackendAssistant.prototype.getHeaderIp = function (headers) {
|
|
419
|
+
headers = headers || {};
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
// these are present for cloudflare requests (11/21/2020)
|
|
423
|
+
headers['cf-connecting-ip']
|
|
424
|
+
|| headers['fastly-temp-xff']
|
|
425
|
+
|
|
426
|
+
// these are present for non-cloudflare requests (11/21/2020)
|
|
427
|
+
|| headers['x-appengine-user-ip']
|
|
428
|
+
|| headers['x-forwarded-for']
|
|
429
|
+
|
|
430
|
+
// Not sure about these
|
|
431
|
+
// || headers['fastly-client-ip']
|
|
432
|
+
|
|
433
|
+
// If unsure, return local IP
|
|
434
|
+
|| '127.0.0.1'
|
|
435
|
+
)
|
|
436
|
+
.split(',')[0]
|
|
437
|
+
.trim();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
BackendAssistant.prototype.getHeaderContinent = function (headers) {
|
|
441
|
+
headers = headers || {};
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
// these are present for cloudflare requests (11/21/2020)
|
|
445
|
+
headers['cf-ipcontinent']
|
|
446
|
+
|
|
447
|
+
// If unsure, return ZZ
|
|
448
|
+
|| 'ZZ'
|
|
449
|
+
)
|
|
450
|
+
.split(',')[0]
|
|
451
|
+
.trim();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
BackendAssistant.prototype.getHeaderCountry = function (headers) {
|
|
455
|
+
headers = headers || {};
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
// these are present for cloudflare requests (11/21/2020)
|
|
459
|
+
headers['cf-ipcountry']
|
|
460
|
+
|
|
461
|
+
//
|
|
462
|
+
|| headers['x-country-code']
|
|
463
|
+
|
|
464
|
+
// these are present for non-cloudflare requests (11/21/2020)
|
|
465
|
+
|| headers['x-appengine-country']
|
|
466
|
+
|
|
467
|
+
// If unsure, return ZZ
|
|
468
|
+
|| 'ZZ'
|
|
469
|
+
)
|
|
470
|
+
.split(',')[0]
|
|
471
|
+
.trim();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
BackendAssistant.prototype.getHeaderRegion = function (headers) {
|
|
475
|
+
headers = headers || {};
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
// these are present for cloudflare requests (11/21/2020)
|
|
479
|
+
headers['cf-region']
|
|
480
|
+
|
|
481
|
+
// these are present for non-cloudflare requests (11/21/2020)
|
|
482
|
+
|| headers['x-appengine-region']
|
|
483
|
+
|
|
484
|
+
// If unsure, return unknown
|
|
485
|
+
|| 'Unknown'
|
|
486
|
+
)
|
|
487
|
+
.split(',')[0]
|
|
488
|
+
.trim();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
BackendAssistant.prototype.getHeaderCity = function (headers) {
|
|
492
|
+
headers = headers || {};
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
// these are present for cloudflare requests (11/21/2020)
|
|
496
|
+
headers['cf-ipcity']
|
|
497
|
+
|
|
498
|
+
|| headers['x-appengine-city']
|
|
499
|
+
|
|
500
|
+
// If unsure, return unknown
|
|
501
|
+
|| 'Unknown'
|
|
502
|
+
)
|
|
503
|
+
.split(',')[0]
|
|
504
|
+
.trim();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
BackendAssistant.prototype.getHeaderLatitude = function (headers) {
|
|
508
|
+
headers = headers || {};
|
|
509
|
+
|
|
510
|
+
return parseFloat((
|
|
511
|
+
// these are present for cloudflare requests (11/21/2020)
|
|
512
|
+
headers['cf-iplatitude']
|
|
513
|
+
|
|
514
|
+
|| (headers['x-appengine-citylatlong'] || '').split(',')[0]
|
|
515
|
+
|
|
516
|
+
// If unsure, return unknown
|
|
517
|
+
|| '0'
|
|
518
|
+
)
|
|
519
|
+
.split(',')[0]
|
|
520
|
+
.trim());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
BackendAssistant.prototype.getHeaderLongitude = function (headers) {
|
|
524
|
+
headers = headers || {};
|
|
525
|
+
|
|
526
|
+
return parseFloat((
|
|
527
|
+
// Cloudflare requests
|
|
528
|
+
headers['cf-iplongitude']
|
|
529
|
+
|
|
530
|
+
|| (headers['x-appengine-citylatlong'] || '').split(',')[1]
|
|
531
|
+
|
|
532
|
+
// If unsure, return unknown
|
|
533
|
+
|| '0'
|
|
534
|
+
)
|
|
535
|
+
.split(',')[0]
|
|
536
|
+
.trim());
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
BackendAssistant.prototype.getHeaderUserAgent = function (headers) {
|
|
541
|
+
headers = headers || {};
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
headers['user-agent']
|
|
545
|
+
|| ''
|
|
546
|
+
)
|
|
547
|
+
.trim();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
BackendAssistant.prototype.getHeaderLanguage = function (headers) {
|
|
551
|
+
headers = headers || {};
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
headers['accept-language']
|
|
555
|
+
|| headers['x-orig-accept-language']
|
|
556
|
+
|| ''
|
|
557
|
+
)
|
|
558
|
+
.trim();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
BackendAssistant.prototype.getHeaderPlatform = function (headers) {
|
|
562
|
+
headers = headers || {};
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
headers['sec-ch-ua-platform']
|
|
566
|
+
|| ''
|
|
567
|
+
)
|
|
568
|
+
.replace(/"/ig, '')
|
|
569
|
+
.trim();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
BackendAssistant.prototype.getHeaderMobile = function (headers) {
|
|
573
|
+
headers = headers || {};
|
|
574
|
+
|
|
575
|
+
// Will be ?0 if fale or ?1 if true
|
|
576
|
+
const mobile = (headers['sec-ch-ua-mobile'] || '').replace(/\?/ig, '');
|
|
577
|
+
|
|
578
|
+
return mobile === '1' || mobile === true || mobile === 'true';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Parses a 'multipart/form-data' upload request
|
|
583
|
+
*
|
|
584
|
+
* @param {Object} req Cloud Function request context.
|
|
585
|
+
* @param {Object} res Cloud Function response context.
|
|
586
|
+
*/
|
|
587
|
+
// https://cloud.google.com/functions/docs/writing/http#multipart_data
|
|
588
|
+
BackendAssistant.prototype.parseMultipartFormData = function (options) {
|
|
589
|
+
const self = this;
|
|
590
|
+
return new Promise(function(resolve, reject) {
|
|
591
|
+
if (!self.initialized) {
|
|
592
|
+
return reject(new Error('Cannot run .parseMultipartForm() until .init() has been called'));
|
|
593
|
+
}
|
|
594
|
+
const existingData = self.request.multipartData;
|
|
595
|
+
// console.log('-----existingData', existingData, Object.keys(_.get(existingData, 'fields', {})).length, Object.keys(_.get(existingData, 'files', {})).length);
|
|
596
|
+
if (Object.keys(_.get(existingData, 'fields', {})).length + Object.keys(_.get(existingData, 'files', {})).length > 0) {
|
|
597
|
+
return resolve(existingData);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
options = options || {};
|
|
601
|
+
|
|
602
|
+
const fs = require('fs');
|
|
603
|
+
const req = self.ref.req;
|
|
604
|
+
const res = self.ref.res;
|
|
605
|
+
|
|
606
|
+
// Node.js doesn't have a built-in multipart/form-data parsing library.
|
|
607
|
+
// Instead, we can use the 'busboy' library from NPM to parse these requests.
|
|
608
|
+
const busboy = require('busboy');
|
|
609
|
+
const jetpack = require('fs-jetpack');
|
|
610
|
+
|
|
611
|
+
// if (req.method !== 'POST') {
|
|
612
|
+
// // Return a "method not allowed" error
|
|
613
|
+
// return res.status(405).end();
|
|
614
|
+
// }
|
|
615
|
+
options.headers = options.headers || req.headers;
|
|
616
|
+
options.limits = options.limits || {};
|
|
617
|
+
|
|
618
|
+
// console.log('++++++++options.headers', options.headers);
|
|
619
|
+
// console.log('++++++++req.rawBody', req.rawBody);
|
|
620
|
+
// console.log('++++++++options.limits', options.limits);
|
|
621
|
+
// console.log('----req.rawBody', req.rawBody);
|
|
622
|
+
|
|
623
|
+
// https://github.com/mscdex/busboy
|
|
624
|
+
// https://github.com/mscdex/busboy/issues/266
|
|
625
|
+
const bb = busboy({
|
|
626
|
+
headers: options.headers,
|
|
627
|
+
limits: options.limits,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// This object will accumulate all the fields, keyed by their name
|
|
631
|
+
const fields = {};
|
|
632
|
+
|
|
633
|
+
// This object will accumulate all the uploaded files, keyed by their name.
|
|
634
|
+
const uploads = {};
|
|
635
|
+
|
|
636
|
+
// This code will process each non-file field in the form.
|
|
637
|
+
bb.on('field', (fieldname, val, info) => {
|
|
638
|
+
// console.log(`Processed field ${fieldname}: ${val}.`);
|
|
639
|
+
fields[fieldname] = val;
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const fileWrites = [];
|
|
643
|
+
|
|
644
|
+
// This code will process each file uploaded.
|
|
645
|
+
bb.on('file', (fieldname, file, info) => {
|
|
646
|
+
// file.on('error', (e) => {
|
|
647
|
+
// console.error('File error', e);
|
|
648
|
+
// });
|
|
649
|
+
// Note: os.tmpdir() points to an in-memory file system on GCF
|
|
650
|
+
// Thus, any files in it must fit in the instance's memory.
|
|
651
|
+
jetpack.dir(self.tmpdir)
|
|
652
|
+
|
|
653
|
+
const filename = info.filename;
|
|
654
|
+
const filepath = path.join(self.tmpdir, filename);
|
|
655
|
+
uploads[fieldname] = filepath;
|
|
656
|
+
const writeStream = fs.createWriteStream(filepath);
|
|
657
|
+
file.pipe(writeStream);
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
// File was processed by Busboy; wait for it to be written.
|
|
661
|
+
// Note: GCF may not persist saved files across invocations.
|
|
662
|
+
// Persistent files must be kept in other locations
|
|
663
|
+
// (such as Cloud Storage buckets).
|
|
664
|
+
const promise = new Promise((resolve, reject) => {
|
|
665
|
+
file.on('end', () => {
|
|
666
|
+
writeStream.end();
|
|
667
|
+
});
|
|
668
|
+
writeStream.on('finish', resolve);
|
|
669
|
+
writeStream.on('error', reject);
|
|
670
|
+
});
|
|
671
|
+
fileWrites.push(promise);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// bb.on('error', async (e) => {
|
|
675
|
+
// console.error('Busboy error', e);
|
|
676
|
+
// })
|
|
677
|
+
|
|
678
|
+
// Triggered once all uploaded files are processed by Busboy.
|
|
679
|
+
// We still need to wait for the disk writes (saves) to complete.
|
|
680
|
+
bb.on('finish', async () => {
|
|
681
|
+
await Promise.all(fileWrites);
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* TODO(developer): Process saved files here
|
|
685
|
+
*/
|
|
686
|
+
// for (const file in uploads) {
|
|
687
|
+
// fs.unlinkSync(uploads[file]);
|
|
688
|
+
// }
|
|
689
|
+
// res.send();
|
|
690
|
+
self.request.multipartData = {
|
|
691
|
+
fields: fields,
|
|
692
|
+
files: uploads,
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return resolve(self.request.multipartData);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Because of an error when using in both Optiic glitch server and ITWCW firebase functions
|
|
699
|
+
if (req.rawBody) {
|
|
700
|
+
return bb.end(req.rawBody);
|
|
701
|
+
} else {
|
|
702
|
+
return req.pipe(bb);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function stringify(obj, replacer, spaces, cycleReplacer) {
|
|
708
|
+
return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// https://github.com/moll/json-stringify-safe/blob/master/stringify.js
|
|
712
|
+
function serializer(replacer, cycleReplacer) {
|
|
713
|
+
var stack = [], keys = []
|
|
714
|
+
|
|
715
|
+
if (cycleReplacer == null) cycleReplacer = function(key, value) {
|
|
716
|
+
if (stack[0] === value) return '[Circular ~]'
|
|
717
|
+
return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return function(key, value) {
|
|
721
|
+
if (stack.length > 0) {
|
|
722
|
+
var thisPos = stack.indexOf(this)
|
|
723
|
+
~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
|
|
724
|
+
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
|
|
725
|
+
if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value)
|
|
726
|
+
}
|
|
727
|
+
else stack.push(value)
|
|
728
|
+
|
|
729
|
+
return replacer == null ? value : replacer.call(this, key, value)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
module.exports = BackendAssistant;
|