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.
- package/package.json +1 -1
- package/src/manager/functions/core/actions/api/admin/get-stats.js +1 -1
- package/src/manager/functions/core/actions/api/admin/send-notification.js +167 -133
- package/src/manager/functions/core/actions/api.js +44 -13
- package/src/manager/functions/core/admin/get-stats.js +1 -1
- package/src/manager/functions/core/events/firestore/{on-subscription.js → notifications/on-write.js} +4 -4
- package/src/manager/helpers/assistant-old-auth.js +116 -0
- package/src/manager/helpers/assistant.js +117 -69
- package/src/manager/helpers/settings.js +3 -3
- package/src/manager/index.js +3 -3
- package/templates/backend-manager-tests.js +4 -4
- package/templates/firestore.rules +4 -4
- package/src/manager/helpers/assistant-new.js +0 -1051
package/package.json
CHANGED
|
@@ -205,7 +205,7 @@ Module.prototype.getAllUsers = function () {
|
|
|
205
205
|
Module.prototype.getAllNotifications = function () {
|
|
206
206
|
const self = this;
|
|
207
207
|
return new Promise(async function(resolve, reject) {
|
|
208
|
-
await self.libraries.admin.firestore().collection('notifications
|
|
208
|
+
await self.libraries.admin.firestore().collection('notifications')
|
|
209
209
|
.get()
|
|
210
210
|
.then(function(querySnapshot) {
|
|
211
211
|
return resolve(querySnapshot.size)
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
1
|
+
// Constants
|
|
2
|
+
const PATH_NOTIFICATIONS = 'notifications';
|
|
3
|
+
const BAD_TOKEN_REASONS = [
|
|
4
|
+
'messaging/invalid-registration-token',
|
|
5
|
+
'messaging/registration-token-not-registered',
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
// Module
|
|
5
9
|
function Module() {
|
|
6
10
|
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
// Main
|
|
9
14
|
Module.prototype.main = function () {
|
|
10
15
|
const self = this;
|
|
16
|
+
|
|
17
|
+
// Shortcuts
|
|
11
18
|
const Manager = self.Manager;
|
|
12
19
|
const Api = self.Api;
|
|
13
20
|
const assistant = self.assistant;
|
|
14
21
|
const payload = self.payload;
|
|
15
22
|
|
|
16
23
|
return new Promise(async function(resolve, reject) {
|
|
17
|
-
|
|
18
24
|
// Set up response obj
|
|
19
25
|
payload.response.data = {
|
|
20
26
|
subscribers: 0,
|
|
@@ -24,33 +30,39 @@ Module.prototype.main = function () {
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
// Fix notification payload
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// Yes, it needs to be NESTED!!! DO NOT REMOVE THE NEST!
|
|
34
|
+
const note = {
|
|
35
|
+
notification: {
|
|
36
|
+
title: payload.data.payload.title || 'Notification',
|
|
37
|
+
body: payload.data.payload.body || 'Check this out',
|
|
38
|
+
icon: payload.data.payload.icon || 'https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/socials/itw-creative-works-brandmark-square-black-1024x1024.png',
|
|
39
|
+
click_action: payload.data.payload.clickAction || 'https://itwcreativeworks.com',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
34
42
|
|
|
43
|
+
// Set notification payload
|
|
35
44
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
const url = new URL(note.notification.click_action);
|
|
46
|
+
url.searchParams.set('cb', new Date().getTime());
|
|
47
|
+
note.notification.click_action = url.toString();
|
|
39
48
|
} catch (e) {
|
|
40
|
-
assistant.errorify(`Failed to add cb to URL: ${e}`, {code:
|
|
49
|
+
reject(assistant.errorify(`Failed to add cb to URL: ${e}`, {code: 400, log: true}));
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
// Log
|
|
53
|
+
assistant.log('Resolved notification payload', note)
|
|
44
54
|
|
|
55
|
+
// Check if user is admin
|
|
45
56
|
if (!payload.user.roles.admin) {
|
|
46
57
|
return reject(assistant.errorify(`Admin required.`, {code: 401}));
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
if
|
|
60
|
+
// Check if title and body are set
|
|
61
|
+
if (!note.notification.title || !note.notification.body) {
|
|
50
62
|
return reject(assistant.errorify(`Parameters <title> and <body> required`, {code: 400, sentry: true}));
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
await self.
|
|
65
|
+
await self.processTokens(note, {tags: false})
|
|
54
66
|
.then(r => {
|
|
55
67
|
return resolve({data: payload.response.data})
|
|
56
68
|
})
|
|
@@ -62,136 +74,158 @@ Module.prototype.main = function () {
|
|
|
62
74
|
};
|
|
63
75
|
|
|
64
76
|
// HELPERS //
|
|
65
|
-
Module.prototype.
|
|
77
|
+
Module.prototype.processTokens = async function (note, options) {
|
|
66
78
|
const self = this;
|
|
79
|
+
|
|
80
|
+
// Shortcuts
|
|
81
|
+
const Manager = self.Manager;
|
|
82
|
+
const Api = self.Api;
|
|
83
|
+
const assistant = self.assistant;
|
|
84
|
+
const payload = self.payload;
|
|
85
|
+
|
|
86
|
+
// Set options
|
|
67
87
|
options = options || {};
|
|
68
88
|
options.tags = options.tags || false;
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
// Define collection path
|
|
91
|
+
const queryConditions = options.tags
|
|
92
|
+
? [{ field: 'tags', operator: 'array-contains-any', value: options.tags }]
|
|
93
|
+
: [];
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let batchSizeMax = 1000;
|
|
85
|
-
|
|
86
|
-
let batchCurrent = [];
|
|
87
|
-
let batchLoops = 1;
|
|
88
|
-
|
|
89
|
-
querySnapshot.forEach(function(doc) {
|
|
90
|
-
// log(self, 'loading... ', batchLoops+'/'+querySnapshot.size);
|
|
91
|
-
if ((batchCurrentSize < batchSizeMax - 1) && (batchLoops < querySnapshot.size)) {
|
|
92
|
-
batchCurrent.push(doc.data().token);
|
|
93
|
-
batchCurrentSize++;
|
|
94
|
-
} else {
|
|
95
|
-
let batchId = batchPromises.length + 1;
|
|
96
|
-
batchCurrent.push(doc.data().token);
|
|
97
|
-
batchCurrentSize++;
|
|
98
|
-
console.log(`Got batch ID: ${batchId} with ${batchCurrentSize} tokens.`);
|
|
99
|
-
batchPromises.push(self.sendBatch(batchCurrent, batchId));
|
|
100
|
-
batchCurrent = [];
|
|
101
|
-
batchCurrentSize = 0;
|
|
102
|
-
self.payload.response.data.batches++;
|
|
103
|
-
}
|
|
104
|
-
batchLoops++;
|
|
105
|
-
});
|
|
106
|
-
})
|
|
107
|
-
.catch(function(e) {
|
|
108
|
-
self.assistant.error('Error querying tokens: ', e)
|
|
109
|
-
reject(error);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
await Promise.all(batchPromises)
|
|
113
|
-
.then(function(values) {
|
|
114
|
-
self.assistant.log('Finished all batches.');
|
|
115
|
-
})
|
|
116
|
-
.catch(function(e) {
|
|
117
|
-
self.assistant.error('Error sending batches: ', e)
|
|
118
|
-
});
|
|
119
|
-
resolve();
|
|
95
|
+
// Batch processing logic
|
|
96
|
+
await Manager.Utilities().iterateCollection(
|
|
97
|
+
async (batch, index) => {
|
|
98
|
+
let batchTokens = [];
|
|
99
|
+
|
|
100
|
+
// Collect tokens from the current batch
|
|
101
|
+
for (const doc of batch.docs) {
|
|
102
|
+
const data = doc.data();
|
|
103
|
+
batchTokens.push(data.token);
|
|
104
|
+
}
|
|
120
105
|
|
|
106
|
+
// Send the batch
|
|
107
|
+
try {
|
|
108
|
+
assistant.log(`Sending batch ${index} with ${batchTokens.length} tokens.`);
|
|
109
|
+
await self.sendBatch(batchTokens, note, index);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
assistant.error(`Error sending batch ${index}`, e);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
collection: PATH_NOTIFICATIONS,
|
|
116
|
+
where: queryConditions,
|
|
117
|
+
batchSize: 1000,
|
|
118
|
+
log: true,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
.then(() => {
|
|
122
|
+
assistant.log('All batches processed successfully.');
|
|
123
|
+
})
|
|
124
|
+
.catch(e => {
|
|
125
|
+
assistant.errorify(`Error during token processing: ${e}`, { code: 500, log: true });
|
|
121
126
|
});
|
|
122
|
-
}
|
|
127
|
+
};
|
|
123
128
|
|
|
124
|
-
Module.prototype.sendBatch = function (batch, id) {
|
|
129
|
+
Module.prototype.sendBatch = async function (batch, note, id) {
|
|
125
130
|
const self = this;
|
|
126
|
-
return new Promise(async function(resolve, reject) {
|
|
127
|
-
// self.assistant.log(`Sending batch ID: ${id}`, batch);
|
|
128
|
-
self.assistant.log(`Sending batch ID: ${id}`);
|
|
129
|
-
|
|
130
|
-
// self.assistant.log('payload', payload);
|
|
131
|
-
|
|
132
|
-
await self.libraries.admin.messaging().sendToDevice(batch, self._notificationPayload)
|
|
133
|
-
.then(async function (response) {
|
|
134
|
-
// self.result.batches.list.push('#' + id + ' | ' + '✅ ' + response.successCount + ' | ' + '❌ ' + response.failureCount);
|
|
135
|
-
self.assistant.log('Sent batch #' + id);
|
|
136
|
-
// self.result.successes += response.successCount;
|
|
137
|
-
// self.result.failures += response.failureCount;
|
|
138
|
-
// console.log('RESP', response);
|
|
139
|
-
if (response.failureCount > 0) {
|
|
140
|
-
await self.cleanTokens(batch, response.results, id);
|
|
141
|
-
}
|
|
142
|
-
self.payload.response.data.sent += (batch.length - response.failureCount);
|
|
143
|
-
resolve();
|
|
144
|
-
})
|
|
145
|
-
.catch(function (e) {
|
|
146
|
-
self.assistant.error('Error sending batch #' + id, e);
|
|
147
|
-
// self.result.status = 'fail';
|
|
148
|
-
reject(e);
|
|
149
|
-
})
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
131
|
|
|
153
|
-
|
|
132
|
+
// Shortcuts
|
|
133
|
+
const Manager = self.Manager;
|
|
134
|
+
const Api = self.Api;
|
|
135
|
+
const assistant = self.assistant;
|
|
136
|
+
const payload = self.payload;
|
|
137
|
+
|
|
138
|
+
// Libraries
|
|
139
|
+
const { admin } = self.libraries;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Log
|
|
143
|
+
assistant.log(`Sending batch ID: ${id}`);
|
|
144
|
+
|
|
145
|
+
// Send the batch
|
|
146
|
+
const response = await admin.messaging().sendToDevice(batch, note);
|
|
147
|
+
|
|
148
|
+
// Log
|
|
149
|
+
assistant.log(`Sent batch ID: ${id}, Success: ${response.successCount}, Failures: ${response.failureCount}`);
|
|
150
|
+
|
|
151
|
+
// Clean bad tokens
|
|
152
|
+
if (response.failureCount > 0) {
|
|
153
|
+
await self.cleanTokens(batch, response.results, id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Update response
|
|
157
|
+
payload.response.data.sent += (batch.length - response.failureCount);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
throw assistant.errorify(`Error sending batch ${id}: ${e}`, { code: 500, log: true });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
Module.prototype.cleanTokens = async function (batch, results, id) {
|
|
154
164
|
const self = this;
|
|
155
165
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
// Shortcuts
|
|
167
|
+
const Manager = self.Manager;
|
|
168
|
+
const Api = self.Api;
|
|
169
|
+
const assistant = self.assistant;
|
|
170
|
+
const payload = self.payload;
|
|
160
171
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
172
|
+
// Log
|
|
173
|
+
assistant.log(`Cleaning tokens of batch ID: ${id}`);
|
|
174
|
+
|
|
175
|
+
// Filter out bad tokens
|
|
176
|
+
const cleanPromises = results
|
|
177
|
+
.map((item, index) => {
|
|
178
|
+
// Check if the token is bad
|
|
179
|
+
if (item.error && BAD_TOKEN_REASONS.includes(item.error.code)) {
|
|
180
|
+
const token = batch[index];
|
|
181
|
+
|
|
182
|
+
// Log
|
|
183
|
+
assistant.log(`Found bad token: ${token} (Reason: ${item.error.code})`);
|
|
184
|
+
|
|
185
|
+
// Delete the token
|
|
186
|
+
return self.deleteToken(token, item.error.code);
|
|
170
187
|
}
|
|
188
|
+
|
|
189
|
+
// Return null for valid tokens
|
|
190
|
+
return null;
|
|
171
191
|
})
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
192
|
+
// Filter out nulls for valid tokens
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
|
|
195
|
+
// Clean bad tokens
|
|
196
|
+
try {
|
|
197
|
+
await Promise.all(cleanPromises);
|
|
198
|
+
assistant.log(`Completed cleaning tokens for batch ID: ${id}`);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
assistant.error(`Error cleaning tokens for batch ID: ${id}`, e);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
179
203
|
|
|
180
|
-
Module.prototype.deleteToken = function (token, errorCode) {
|
|
204
|
+
Module.prototype.deleteToken = async function (token, errorCode) {
|
|
181
205
|
const self = this;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
206
|
+
|
|
207
|
+
// Shortcuts
|
|
208
|
+
const Manager = self.Manager;
|
|
209
|
+
const Api = self.Api;
|
|
210
|
+
const assistant = self.assistant;
|
|
211
|
+
const payload = self.payload;
|
|
212
|
+
|
|
213
|
+
// Libraries
|
|
214
|
+
const { admin } = self.libraries;
|
|
215
|
+
|
|
216
|
+
// Delete the token
|
|
217
|
+
try {
|
|
218
|
+
// Delete the token
|
|
219
|
+
await admin.firestore().doc(`${PATH_NOTIFICATIONS}/${token}`).delete();
|
|
220
|
+
|
|
221
|
+
// Log
|
|
222
|
+
assistant.log(`Deleted bad token: ${token} (Reason: ${errorCode})`);
|
|
223
|
+
|
|
224
|
+
// Update response
|
|
225
|
+
payload.response.data.deleted++;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
assistant.error(`Failed to delete bad token: ${token} (Reason: ${errorCode}). Error: ${error}`);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
196
230
|
|
|
197
231
|
module.exports = Module;
|
|
@@ -9,6 +9,8 @@ function Module() {
|
|
|
9
9
|
|
|
10
10
|
Module.prototype.init = function (Manager, data) {
|
|
11
11
|
const self = this;
|
|
12
|
+
|
|
13
|
+
// Set properties
|
|
12
14
|
self.Manager = Manager;
|
|
13
15
|
self.libraries = Manager.libraries;
|
|
14
16
|
self.assistant = Manager.Assistant({req: data.req, res: data.res})
|
|
@@ -30,15 +32,14 @@ Module.prototype.init = function (Manager, data) {
|
|
|
30
32
|
self.assistant.request.data.payload = self.assistant.request.data.payload || {};
|
|
31
33
|
self.assistant.request.data.options = self.assistant.request.data.options || {};
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
self.assistant.log(`Executing (log): ${resolved.command}`, self.assistant.request, JSON.stringify(self.assistant.request))
|
|
35
|
-
}
|
|
36
|
-
|
|
35
|
+
// Return
|
|
37
36
|
return self;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
Module.prototype.main = function() {
|
|
41
40
|
const self = this;
|
|
41
|
+
|
|
42
|
+
// Shortcuts
|
|
42
43
|
const Manager = self.Manager;
|
|
43
44
|
const libraries = self.libraries;
|
|
44
45
|
const assistant = self.assistant;
|
|
@@ -47,19 +48,34 @@ Module.prototype.main = function() {
|
|
|
47
48
|
|
|
48
49
|
return new Promise(async function(resolve, reject) {
|
|
49
50
|
return libraries.cors(req, res, async () => {
|
|
51
|
+
// Set properties
|
|
50
52
|
self.payload.data = assistant.request.data;
|
|
51
53
|
self.payload.user = await assistant.authenticate();
|
|
52
54
|
|
|
55
|
+
// Set properties
|
|
56
|
+
const headers = assistant.request.headers;
|
|
57
|
+
const method = assistant.request.method;
|
|
58
|
+
const url = assistant.request.url;
|
|
59
|
+
const geolocation = assistant.request.geolocation;
|
|
60
|
+
const client = assistant.request.client;
|
|
61
|
+
|
|
62
|
+
// Strip URL
|
|
63
|
+
const strippedUrl = stripUrl(url);
|
|
64
|
+
|
|
53
65
|
// Quit if OPTIONS request
|
|
54
|
-
if (
|
|
66
|
+
if (method === 'OPTIONS') {
|
|
55
67
|
return resolve();
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
// Resolve command
|
|
59
71
|
const resolved = self.resolveCommand(self.payload.data.command);
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
// Log
|
|
74
|
+
// assistant.log(`Executing: ${resolved.command}`, self.payload, JSON.stringify(self.payload))
|
|
75
|
+
// assistant.log(`Resolved URL: ${Manager.project.functionsUrl}?command=${encodeURIComponent(resolved.command)}&payload=${encodeURIComponent(JSON.stringify(self.assistant.request.data.payload))}`)
|
|
76
|
+
assistant.log(`bm_api(${resolved.command}): Request (${geolocation.ip} @ ${geolocation.country}, ${geolocation.region}, ${geolocation.city}) [${method} > ${strippedUrl}]`, JSON.stringify(assistant.request.data));
|
|
77
|
+
assistant.log(`bm_api(${resolved.command}): Headers`, JSON.stringify(headers));
|
|
78
|
+
|
|
63
79
|
|
|
64
80
|
// Set up options
|
|
65
81
|
self.payload.data.options = self.payload.data.options || {};
|
|
@@ -70,7 +86,7 @@ Module.prototype.main = function() {
|
|
|
70
86
|
let delay = Math.floor(self.payload.data.options.delay / 1000);
|
|
71
87
|
|
|
72
88
|
await powertools.poll(() => {
|
|
73
|
-
|
|
89
|
+
assistant.log(`Delaying for ${delay--} seconds...`);
|
|
74
90
|
}, {interval: 1000, timeout: self.payload.data.options.delay})
|
|
75
91
|
.catch(e => e);
|
|
76
92
|
}
|
|
@@ -116,7 +132,7 @@ Module.prototype.main = function() {
|
|
|
116
132
|
|
|
117
133
|
// Send response
|
|
118
134
|
if (self.payload.response.status >= 200 && self.payload.response.status < 399) {
|
|
119
|
-
|
|
135
|
+
assistant.log(`Finished ${resolved.command} (status=${self.payload.response.status})`, self.payload, JSON.stringify(self.payload))
|
|
120
136
|
|
|
121
137
|
if (self.payload.response.redirect) {
|
|
122
138
|
res.redirect(self.payload.response.redirect);
|
|
@@ -126,7 +142,7 @@ Module.prototype.main = function() {
|
|
|
126
142
|
return resolve();
|
|
127
143
|
}
|
|
128
144
|
} else {
|
|
129
|
-
|
|
145
|
+
assistant.error(`Error executing ${resolved.command} @ ${resolved.path} (status=${self.payload.response.status}):`, self.payload.response.error)
|
|
130
146
|
res.send(`${self.payload.response.error}`)
|
|
131
147
|
return reject(self.payload.response.error);
|
|
132
148
|
}
|
|
@@ -188,8 +204,17 @@ Module.prototype.import = function (command, payload, user, response) {
|
|
|
188
204
|
|
|
189
205
|
Module.prototype.resolveCommand = function (command) {
|
|
190
206
|
const self = this;
|
|
207
|
+
|
|
208
|
+
// Shortcuts
|
|
209
|
+
const assistant = self.assistant;
|
|
210
|
+
|
|
211
|
+
// Set original command
|
|
191
212
|
const originalCommand = command;
|
|
192
213
|
|
|
214
|
+
// Set properties
|
|
215
|
+
const method = assistant.request.method;
|
|
216
|
+
|
|
217
|
+
// Set command
|
|
193
218
|
command = command || '';
|
|
194
219
|
|
|
195
220
|
// Start
|
|
@@ -251,9 +276,9 @@ Module.prototype.resolveCommand = function (command) {
|
|
|
251
276
|
// Check local path
|
|
252
277
|
const resolvedPath = self.resolveApiPath(command);
|
|
253
278
|
|
|
254
|
-
// if
|
|
255
|
-
if (!resolvedPath) {
|
|
256
|
-
|
|
279
|
+
// Log if command does not exist
|
|
280
|
+
if (method !== 'OPTIONS' && !resolvedPath) {
|
|
281
|
+
assistant.error(`This command does not exist: ${originalCommand} => ${command} @ ${resolvedPath}`)
|
|
257
282
|
}
|
|
258
283
|
|
|
259
284
|
return {
|
|
@@ -354,4 +379,10 @@ Module.prototype.resolveApiPath = function (command) {
|
|
|
354
379
|
}
|
|
355
380
|
};
|
|
356
381
|
|
|
382
|
+
function stripUrl(url) {
|
|
383
|
+
const newUrl = new URL(url);
|
|
384
|
+
|
|
385
|
+
return `${newUrl.hostname}${newUrl.pathname}`.replace(/\/$/, '');
|
|
386
|
+
}
|
|
387
|
+
|
|
357
388
|
module.exports = Module;
|
|
@@ -179,7 +179,7 @@ let Module = {
|
|
|
179
179
|
getAllSubscriptions: function () {
|
|
180
180
|
let self = this;
|
|
181
181
|
return new Promise(async function(resolve, reject) {
|
|
182
|
-
await self.libraries.admin.firestore().collection('notifications
|
|
182
|
+
await self.libraries.admin.firestore().collection('notifications')
|
|
183
183
|
.get()
|
|
184
184
|
.then(function(querySnapshot) {
|
|
185
185
|
return resolve(querySnapshot.size)
|
package/src/manager/functions/core/events/firestore/{on-subscription.js → notifications/on-write.js}
RENAMED
|
@@ -49,12 +49,12 @@ Module.prototype.main = function () {
|
|
|
49
49
|
if (eventType === 'delete') {
|
|
50
50
|
await libraries.admin.firestore().doc(`meta/stats`)
|
|
51
51
|
.update({
|
|
52
|
-
'
|
|
52
|
+
'notifications.total': libraries.admin.firestore.FieldValue.increment(-1),
|
|
53
53
|
})
|
|
54
54
|
.then(r => {
|
|
55
55
|
analytics = self.Manager.Analytics({
|
|
56
56
|
assistant: assistant,
|
|
57
|
-
uuid:
|
|
57
|
+
uuid: dataBefore?.owner?.uid,
|
|
58
58
|
})
|
|
59
59
|
.event({
|
|
60
60
|
name: 'notification-unsubscribe',
|
|
@@ -78,12 +78,12 @@ Module.prototype.main = function () {
|
|
|
78
78
|
} else if (eventType === 'create') {
|
|
79
79
|
await libraries.admin.firestore().doc(`meta/stats`)
|
|
80
80
|
.update({
|
|
81
|
-
'
|
|
81
|
+
'notifications.total': libraries.admin.firestore.FieldValue.increment(1),
|
|
82
82
|
})
|
|
83
83
|
.then(r => {
|
|
84
84
|
analytics = self.Manager.Analytics({
|
|
85
85
|
assistant: assistant,
|
|
86
|
-
uuid:
|
|
86
|
+
uuid: dataAfter?.owner?.uid,
|
|
87
87
|
})
|
|
88
88
|
.event({
|
|
89
89
|
name: 'notification-subscribe',
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
BackendAssistant.prototype.authenticate = async function (options) {
|
|
2
|
+
const self = this;
|
|
3
|
+
|
|
4
|
+
// Shortcuts
|
|
5
|
+
let admin = self.ref.admin;
|
|
6
|
+
let functions = self.ref.functions;
|
|
7
|
+
let req = self.ref.req;
|
|
8
|
+
let res = self.ref.res;
|
|
9
|
+
let data = self.request.data;
|
|
10
|
+
let idToken;
|
|
11
|
+
|
|
12
|
+
options = options || {};
|
|
13
|
+
options.resolve = typeof options.resolve === 'undefined' ? true : options.resolve;
|
|
14
|
+
|
|
15
|
+
function _resolve(user) {
|
|
16
|
+
user = user || {};
|
|
17
|
+
user.authenticated = typeof user.authenticated === 'undefined'
|
|
18
|
+
? false
|
|
19
|
+
: user.authenticated;
|
|
20
|
+
|
|
21
|
+
if (options.resolve) {
|
|
22
|
+
self.request.user = self.resolveAccount(user);
|
|
23
|
+
return self.request.user;
|
|
24
|
+
} else {
|
|
25
|
+
return user;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (req?.headers?.authorization && req?.headers?.authorization?.startsWith('Bearer ')) {
|
|
30
|
+
// Read the ID Token from the Authorization header.
|
|
31
|
+
idToken = req.headers.authorization.split('Bearer ')[1];
|
|
32
|
+
self.log('Found "Authorization" header', idToken);
|
|
33
|
+
} else if (req?.cookies?.__session) {
|
|
34
|
+
// Read the ID Token from cookie.
|
|
35
|
+
idToken = req.cookies.__session;
|
|
36
|
+
self.log('Found "__session" cookie', idToken);
|
|
37
|
+
} else if (data.backendManagerKey || data.authenticationToken) {
|
|
38
|
+
// Check with custom BEM Token
|
|
39
|
+
let storedApiKey;
|
|
40
|
+
try {
|
|
41
|
+
// Disabled this 5/11/24 because i dont know why we would need to do functions.config() if we already have the Manager
|
|
42
|
+
// const workingConfig = self.Manager?.config || functions.config();
|
|
43
|
+
storedApiKey = self.Manager?.config?.backend_manager?.key || '';
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Do nothing
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Set idToken as working token of either backendManagerKey or authenticationToken
|
|
49
|
+
idToken = data.backendManagerKey || data.authenticationToken;
|
|
50
|
+
|
|
51
|
+
// Log the token
|
|
52
|
+
self.log('Found "backendManagerKey" or "authenticationToken" parameter', {storedApiKey: storedApiKey, idToken: idToken});
|
|
53
|
+
|
|
54
|
+
// Check if the token is correct
|
|
55
|
+
if (storedApiKey && (storedApiKey === data.backendManagerKey || storedApiKey === data.authenticationToken)) {
|
|
56
|
+
self.request.user.authenticated = true;
|
|
57
|
+
self.request.user.roles.admin = true;
|
|
58
|
+
return _resolve(self.request.user);
|
|
59
|
+
}
|
|
60
|
+
} else if (options.apiKey || data.apiKey) {
|
|
61
|
+
const apiKey = options.apiKey || data.apiKey;
|
|
62
|
+
self.log('Found "options.apiKey"', apiKey);
|
|
63
|
+
|
|
64
|
+
if (apiKey.includes('test')) {
|
|
65
|
+
return _resolve(self.request.user);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await admin.firestore().collection(`users`)
|
|
69
|
+
.where('api.privateKey', '==', apiKey)
|
|
70
|
+
.get()
|
|
71
|
+
.then(function(querySnapshot) {
|
|
72
|
+
querySnapshot.forEach(function(doc) {
|
|
73
|
+
self.request.user = doc.data();
|
|
74
|
+
self.request.user.authenticated = true;
|
|
75
|
+
});
|
|
76
|
+
})
|
|
77
|
+
.catch(function(error) {
|
|
78
|
+
console.error('Error getting documents: ', error);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return _resolve(self.request.user);
|
|
82
|
+
} else {
|
|
83
|
+
// self.log('No Firebase ID token was able to be extracted.',
|
|
84
|
+
// 'Make sure you authenticate your request by providing either the following HTTP header:',
|
|
85
|
+
// 'Authorization: Bearer <Firebase ID Token>',
|
|
86
|
+
// 'or by passing a "__session" cookie',
|
|
87
|
+
// 'or by passing backendManagerKey or authenticationToken in the body or query');
|
|
88
|
+
|
|
89
|
+
return _resolve(self.request.user);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check with firebase
|
|
93
|
+
try {
|
|
94
|
+
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
|
|
95
|
+
if (options.debug) {
|
|
96
|
+
self.log('Token correctly decoded', decodedIdToken.email, decodedIdToken.user_id);
|
|
97
|
+
}
|
|
98
|
+
await admin.firestore().doc(`users/${decodedIdToken.user_id}`)
|
|
99
|
+
.get()
|
|
100
|
+
.then(async function (doc) {
|
|
101
|
+
if (doc.exists) {
|
|
102
|
+
self.request.user = Object.assign({}, self.request.user, doc.data());
|
|
103
|
+
}
|
|
104
|
+
self.request.user.authenticated = true;
|
|
105
|
+
self.request.user.auth.uid = decodedIdToken.user_id;
|
|
106
|
+
self.request.user.auth.email = decodedIdToken.email;
|
|
107
|
+
if (options.debug) {
|
|
108
|
+
self.log('Found user doc', self.request.user)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
return _resolve(self.request.user);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
self.error('Error while verifying Firebase ID token:', error);
|
|
114
|
+
return _resolve(self.request.user);
|
|
115
|
+
}
|
|
116
|
+
};
|