backend-manager 5.0.100 → 5.0.101

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": "5.0.100",
3
+ "version": "5.0.101",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Data requests cron job
3
+ *
4
+ * Processes data request status transitions:
5
+ * - pending → complete: 14 days after creation
6
+ * - complete → expired: 30 days after becoming complete (44 days after creation)
7
+ *
8
+ * Scans the entire collection (no index required) since data-requests is small.
9
+ */
10
+ module.exports = async ({ Manager, assistant, context, libraries }) => {
11
+ const { admin } = libraries;
12
+ const nowUNIX = Math.round(Date.now() / 1000);
13
+
14
+ const FOURTEEN_DAYS = 14 * 24 * 60 * 60;
15
+ const FORTY_FOUR_DAYS = 44 * 24 * 60 * 60;
16
+
17
+ assistant.log('Starting...');
18
+
19
+ // Only fetch requests created within the last 45 days (single-field filter, no composite index needed)
20
+ const snapshot = await admin.firestore()
21
+ .collection('data-requests')
22
+ .where('metadata.created.timestampUNIX', '>', nowUNIX - FORTY_FOUR_DAYS - 86400)
23
+ .get();
24
+
25
+ assistant.log(`Found ${snapshot.size} total data requests`);
26
+
27
+ let completed = 0;
28
+ let expired = 0;
29
+
30
+ for (const doc of snapshot.docs) {
31
+ const data = doc.data();
32
+ const createdUNIX = data.metadata?.created?.timestampUNIX || 0;
33
+ const age = nowUNIX - createdUNIX;
34
+
35
+ if (data.status === 'pending' && age >= FOURTEEN_DAYS) {
36
+ await doc.ref.update({ status: 'complete' })
37
+ .then(() => {
38
+ completed++;
39
+ assistant.log(`Completed request ${doc.id} (age: ${Math.round(age / 86400)}d)`);
40
+ })
41
+ .catch((e) => {
42
+ assistant.error(`Failed to complete request ${doc.id}: ${e.message}`);
43
+ });
44
+ } else if (data.status === 'complete' && age >= FORTY_FOUR_DAYS) {
45
+ await doc.ref.update({ status: 'expired' })
46
+ .then(() => {
47
+ expired++;
48
+ assistant.log(`Expired request ${doc.id} (age: ${Math.round(age / 86400)}d)`);
49
+ })
50
+ .catch((e) => {
51
+ assistant.error(`Failed to expire request ${doc.id}: ${e.message}`);
52
+ });
53
+ }
54
+ }
55
+
56
+ assistant.log(`Completed! (${completed} completed, ${expired} expired)`);
57
+ };
@@ -32,8 +32,51 @@ module.exports = async ({ assistant, user, libraries }) => {
32
32
 
33
33
  assistant.log(`Data request cancelled: ${requestDoc.id} for user ${uid}`);
34
34
 
35
+ // Send cancellation email (fire-and-forget)
36
+ sendCancellationEmail(assistant, user, requestDoc.id);
37
+
35
38
  return assistant.respond({
36
39
  message: 'Your data request has been cancelled.',
37
40
  request: { id: requestDoc.id, ...request },
38
41
  });
39
42
  };
43
+
44
+ /**
45
+ * Send data request cancellation email (fire-and-forget)
46
+ */
47
+ function sendCancellationEmail(assistant, user, requestId) {
48
+ const Manager = assistant.Manager;
49
+ const mailer = Manager.Email(assistant);
50
+ const uid = user.auth.uid;
51
+
52
+ mailer.send({
53
+ to: user.auth.email,
54
+ categories: ['account/data-request-cancelled'],
55
+ subject: `Your data request has been cancelled #${requestId}`,
56
+ template: 'default',
57
+ group: 'account',
58
+ copy: true,
59
+ data: {
60
+ email: {
61
+ preview: `Your data export request #${requestId} has been cancelled.`,
62
+ },
63
+ body: {
64
+ title: 'Data Request Cancelled',
65
+ message: `Your data export request has been cancelled as requested.
66
+
67
+ - **Request reference:** #${requestId}
68
+ - **Account UID:** ${uid}
69
+
70
+ You may submit a new data request at any time from your account page.
71
+
72
+ If you did not cancel this request, please contact us immediately by replying to this email.`,
73
+ },
74
+ },
75
+ })
76
+ .then((result) => {
77
+ assistant.log(`sendCancellationEmail(): Success, status=${result.status}`);
78
+ })
79
+ .catch((e) => {
80
+ assistant.error(`sendCancellationEmail(): Failed: ${e.message}`);
81
+ });
82
+ }
@@ -174,8 +174,54 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
174
174
 
175
175
  assistant.log(`Data request ${requestId} downloaded by user ${uid} (download #${downloads})`);
176
176
 
177
+ // Send download confirmation email (fire-and-forget)
178
+ sendDownloadEmail(assistant, user, requestId, downloads);
179
+
177
180
  return assistant.respond({
178
181
  request: { id: requestId, ...request, downloads: downloads },
179
182
  data: data,
180
183
  });
181
184
  };
185
+
186
+ /**
187
+ * Send data download confirmation email (fire-and-forget)
188
+ */
189
+ function sendDownloadEmail(assistant, user, requestId, downloads) {
190
+ const Manager = assistant.Manager;
191
+ const mailer = Manager.Email(assistant);
192
+ const uid = user.auth.uid;
193
+ const downloadDate = assistant.meta.startTime.timestamp;
194
+
195
+ mailer.send({
196
+ to: user.auth.email,
197
+ categories: ['account/data-request-download'],
198
+ subject: `Your data has been downloaded #${requestId}`,
199
+ template: 'default',
200
+ group: 'account',
201
+ copy: true,
202
+ data: {
203
+ email: {
204
+ preview: `Your personal data export was downloaded on ${downloadDate}.`,
205
+ },
206
+ body: {
207
+ title: 'Data Download Confirmation',
208
+ message: `Your personal data export has been successfully downloaded.
209
+
210
+ **Download details:**
211
+
212
+ - **Date:** ${downloadDate}
213
+ - **Download #:** ${downloads}
214
+ - **Request reference:** #${requestId}
215
+ - **Account UID:** ${uid}
216
+
217
+ If you did not initiate this download, please secure your account immediately and contact us by replying to this email.`,
218
+ },
219
+ },
220
+ })
221
+ .then((result) => {
222
+ assistant.log(`sendDownloadEmail(): Success, status=${result.status}`);
223
+ })
224
+ .catch((e) => {
225
+ assistant.error(`sendDownloadEmail(): Failed: ${e.message}`);
226
+ });
227
+ }
@@ -77,7 +77,7 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
77
77
  assistant.log(`Data request created: ${requestId} for user ${uid}`);
78
78
 
79
79
  // Send confirmation email (fire-and-forget)
80
- sendConfirmationEmail(assistant, user.auth.email, requestId, reason);
80
+ sendConfirmationEmail(assistant, user, requestId, reason);
81
81
 
82
82
  return assistant.respond({
83
83
  request: { id: requestId, ...docData },
@@ -87,16 +87,16 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
87
87
  /**
88
88
  * Send data request confirmation email (fire-and-forget)
89
89
  */
90
- function sendConfirmationEmail(assistant, email, requestId, reason) {
90
+ function sendConfirmationEmail(assistant, user, requestId, reason) {
91
91
  const Manager = assistant.Manager;
92
- const brandName = Manager.config.brand.name;
93
92
  const mailer = Manager.Email(assistant);
93
+ const uid = user.auth.uid;
94
94
  const reasonLine = reason
95
95
  ? `\n\n**Reason provided:** ${reason}`
96
96
  : '';
97
97
 
98
98
  mailer.send({
99
- to: email,
99
+ to: user.auth.email,
100
100
  categories: ['account/data-request'],
101
101
  subject: `Your data request has been received #${requestId}`,
102
102
  template: 'default',
@@ -120,7 +120,8 @@ function sendConfirmationEmail(assistant, email, requestId, reason) {
120
120
 
121
121
  If you did not make this request, please contact us immediately by replying to this email.
122
122
 
123
- Reference: **#${requestId}**`,
123
+ - **Reference:** #${requestId}
124
+ - **Account UID:** ${uid}`,
124
125
  },
125
126
  },
126
127
  })
@@ -12,8 +12,9 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
12
12
  return assistant.respond('Authentication required', { code: 401 });
13
13
  }
14
14
 
15
- // Get target UID
15
+ // Get target UID and reason
16
16
  const uid = settings.uid;
17
+ const reason = (settings.reason || '').replace(/<[^>]*>/g, '').trim().substring(0, 500);
17
18
 
18
19
  // Require admin to delete other users
19
20
  if (uid !== user.auth.uid && !user.roles.admin) {
@@ -75,9 +76,11 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
75
76
  return assistant.respond(`Failed to delete user: ${e}`, { code: 500 });
76
77
  }
77
78
 
79
+ assistant.log(`Account deleted: ${uid}${reason ? `, reason: ${reason}` : ''}`);
80
+
78
81
  // Send confirmation email (fire-and-forget)
79
82
  if (email) {
80
- sendConfirmationEmail(assistant, email);
83
+ sendConfirmationEmail(assistant, email, reason);
81
84
  }
82
85
 
83
86
  return assistant.respond({ success: true });
@@ -86,10 +89,13 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
86
89
  /**
87
90
  * Send account deletion confirmation email (fire-and-forget)
88
91
  */
89
- function sendConfirmationEmail(assistant, email) {
92
+ function sendConfirmationEmail(assistant, email, reason) {
90
93
  const Manager = assistant.Manager;
91
94
  const brandName = Manager.config.brand.name;
92
95
  const mailer = Manager.Email(assistant);
96
+ const reasonLine = reason
97
+ ? `\n\n**Reason provided:** ${reason}`
98
+ : '';
93
99
 
94
100
  mailer.send({
95
101
  to: email,
@@ -97,13 +103,14 @@ function sendConfirmationEmail(assistant, email) {
97
103
  subject: `Your ${brandName} account has been deleted`,
98
104
  template: 'default',
99
105
  group: 'account',
106
+ copy: true,
100
107
  data: {
101
108
  email: {
102
109
  preview: `Your ${brandName} account has been permanently deleted. All associated data has been removed.`,
103
110
  },
104
111
  body: {
105
112
  title: 'Account Deleted',
106
- message: `Your **${brandName}** account and all associated personal data have been permanently deleted from our systems. This action is irreversible.
113
+ message: `Your **${brandName}** account and all associated personal data have been permanently deleted from our systems. This action is irreversible.${reasonLine}
107
114
 
108
115
  **What this means:**
109
116
 
@@ -4,4 +4,9 @@ module.exports = ({ user }) => ({
4
4
  default: user?.auth?.uid,
5
5
  required: false,
6
6
  },
7
+ reason: {
8
+ types: ['string'],
9
+ default: '',
10
+ max: 500,
11
+ },
7
12
  });