abmp-npm 1.8.43 → 1.8.45

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.
Files changed (50) hide show
  1. package/CONTACT_EMAIL_UPDATE_DEBUG.md +313 -0
  2. package/DEBUG_QUICKSTART.md +133 -0
  3. package/backend/cms-data-methods.js +8 -0
  4. package/backend/consts.js +22 -7
  5. package/backend/contacts-methods-DEBUG.js +237 -0
  6. package/backend/contacts-methods-TEST.js +271 -0
  7. package/backend/contacts-methods.js +6 -1
  8. package/backend/daily-pull/consts.js +0 -3
  9. package/backend/daily-pull/process-member-methods.js +1 -1
  10. package/backend/daily-pull/sync-to-cms-methods.js +10 -6
  11. package/backend/daily-pull/utils.js +3 -3
  12. package/backend/data-hooks.js +29 -0
  13. package/backend/elevated-modules.js +2 -0
  14. package/backend/http-functions/httpFunctions.js +86 -0
  15. package/backend/http-functions/index.js +3 -0
  16. package/backend/http-functions/interests.js +37 -0
  17. package/backend/index.js +6 -1
  18. package/backend/jobs.js +15 -3
  19. package/backend/login/index.js +7 -0
  20. package/backend/login/login-methods-factory.js +24 -0
  21. package/backend/login/qa-login-methods.js +72 -0
  22. package/backend/login/sso-methods.js +158 -0
  23. package/backend/members-data-methods.js +271 -94
  24. package/backend/pac-api-methods.js +3 -4
  25. package/backend/routers/index.js +3 -0
  26. package/backend/routers/methods.js +177 -0
  27. package/backend/routers/utils.js +118 -0
  28. package/backend/search-filters-methods.js +3 -0
  29. package/backend/tasks/consts.js +19 -0
  30. package/backend/tasks/index.js +6 -0
  31. package/backend/tasks/migration-methods.js +26 -0
  32. package/backend/tasks/tasks-configs.js +124 -0
  33. package/backend/tasks/tasks-helpers-methods.js +419 -0
  34. package/backend/tasks/tasks-process-methods.js +545 -0
  35. package/backend/test-methods.js +118 -0
  36. package/backend/utils.js +85 -41
  37. package/package.json +13 -2
  38. package/pages/LoadingPage.js +20 -0
  39. package/pages/Profile.js +2 -2
  40. package/pages/QAPage.js +39 -0
  41. package/pages/SaveAlerts.js +13 -0
  42. package/pages/SelectBannerImages.js +46 -0
  43. package/pages/deleteConfirm.js +19 -0
  44. package/pages/index.js +5 -0
  45. package/pages/personalDetails.js +12 -8
  46. package/public/consts.js +6 -23
  47. package/public/sso-auth-methods.js +43 -0
  48. package/backend/routers-methods.js +0 -186
  49. package/backend/routers-utils.js +0 -158
  50. package/backend/tasks.js +0 -37
@@ -0,0 +1,419 @@
1
+ const crypto = require('crypto');
2
+
3
+ const { files } = require('@wix/media');
4
+ const aws4 = require('aws4');
5
+ const axios = require('axios');
6
+
7
+ const { PAGES_PATHS } = require('../../public/consts');
8
+ const { findMemberByWixDataId, updateMember } = require('../members-data-methods');
9
+ const { getSecret, getSiteBaseUrl, encodeXml, formatDateOnly } = require('../utils');
10
+
11
+ async function getServerlessAuth() {
12
+ const serverlessAuth = await getSecret('serverless_auth');
13
+ return serverlessAuth;
14
+ }
15
+
16
+ function isValidImageUrl(url) {
17
+ if (!url || typeof url !== 'string') return false;
18
+
19
+ // Check for valid URL format
20
+ let parsedUrl;
21
+ try {
22
+ parsedUrl = new URL(url);
23
+ } catch {
24
+ return false;
25
+ }
26
+
27
+ // Only allow HTTP and HTTPS protocols (reject blob:, data:, file:, etc.)
28
+ const validProtocols = ['http:', 'https:'];
29
+ if (!validProtocols.includes(parsedUrl.protocol)) {
30
+ return false;
31
+ }
32
+
33
+ // Extract file extension from URL (handle query parameters)
34
+ const urlPath = url.split('?')[0].toLowerCase();
35
+ const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
36
+
37
+ // Check if URL ends with valid extension
38
+ const hasValidExtension = validExtensions.some(ext => urlPath.endsWith(ext));
39
+
40
+ // Reject obviously invalid extensions
41
+ const invalidExtensions = [
42
+ '.pdf',
43
+ '.doc',
44
+ '.docx',
45
+ '.txt',
46
+ '.ps',
47
+ '.html',
48
+ '.htm',
49
+ '_jpg',
50
+ '_png',
51
+ '_gif',
52
+ ];
53
+ const hasInvalidExtension = invalidExtensions.some(ext => urlPath.includes(ext));
54
+
55
+ return hasValidExtension && !hasInvalidExtension;
56
+ }
57
+
58
+ function isValidContentType(contentType) {
59
+ if (!contentType) return false;
60
+
61
+ const contentTypeLower = contentType.toLowerCase();
62
+
63
+ // Valid image content types
64
+ const validTypes = [
65
+ 'image/jpeg',
66
+ 'image/jpg',
67
+ 'image/png',
68
+ 'image/gif',
69
+ 'image/webp',
70
+ 'image/bmp',
71
+ ];
72
+
73
+ // Explicitly reject non-image content types
74
+ const invalidTypes = [
75
+ 'application/pdf',
76
+ 'application/msword',
77
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
78
+ 'text/plain',
79
+ 'text/html',
80
+ 'text/htm',
81
+ 'application/octet-stream',
82
+ ];
83
+
84
+ if (invalidTypes.some(type => contentTypeLower.includes(type))) {
85
+ return false;
86
+ }
87
+
88
+ return validTypes.includes(contentTypeLower);
89
+ }
90
+ async function updateMemberRichContent(memberId) {
91
+ console.log('starting to call http function for member', memberId);
92
+
93
+ const member = await findMemberByWixDataId(memberId);
94
+ const htmlString = member.aboutYouHtml;
95
+ const raw = JSON.stringify({
96
+ content: htmlString,
97
+ });
98
+
99
+ const requestOptions = {
100
+ method: 'post',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ Cookie: 'XSRF-TOKEN=1753949844|p--a7HsuVjR4',
104
+ Authorization: 'Bearer ' + (await getServerlessAuth()),
105
+ },
106
+ body: raw,
107
+ };
108
+
109
+ try {
110
+ const response = await fetch(
111
+ 'https://www.wixapis.com/data-sync/v1/abmp-content-converter',
112
+ requestOptions
113
+ );
114
+ if (response.ok) {
115
+ const data = await response.json();
116
+ const updatedMember = {
117
+ ...member,
118
+ aboutYourSelf: data.richContent.richContent,
119
+ aboutYouText: data.plainText.plainText,
120
+ };
121
+ if (data.richContent.status != 'VALID' || data.plainText.status != 'VALID') {
122
+ console.error(`updateMemberRichContent faield for member: ${memberId} `, {
123
+ memberId,
124
+ raw,
125
+ data,
126
+ });
127
+ }
128
+ console.log('updatedMember **********', updatedMember);
129
+ await updateMember(updatedMember);
130
+ console.log('rich content added successfully for member with id: ', memberId);
131
+ } else {
132
+ console.error(`error in fetching data for member ID: ${memberId}, response: ${response}`);
133
+ }
134
+ } catch (error) {
135
+ console.error('error in fetching data', error);
136
+ }
137
+ }
138
+ async function updateMemberProfileImage(memberId) {
139
+ try {
140
+ const member = await findMemberByWixDataId(memberId);
141
+
142
+ // Check if member has an external profile image URL
143
+ if (!member.profileImage || member.profileImage.startsWith('wix:')) {
144
+ console.log(`Member ${memberId} already has Wix-hosted image or no image`);
145
+ return { success: true, message: 'No update needed' };
146
+ }
147
+
148
+ // Validate image URL format before attempting download
149
+ if (!isValidImageUrl(member.profileImage)) {
150
+ console.log(`Member ${memberId} has invalid image URL format: ${member.profileImage}`);
151
+ return { success: true, message: 'Invalid image URL format - skipped' };
152
+ }
153
+
154
+ const response = await axios.get(member.profileImage, {
155
+ responseType: 'arraybuffer',
156
+ headers: {
157
+ 'User-Agent':
158
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
159
+ Accept: 'image/webp,image/apng,image/*,*/*;q=0.8',
160
+ 'Accept-Language': 'en-US,en;q=0.9',
161
+ 'Cache-Control': 'no-cache',
162
+ },
163
+ timeout: 10000, // 10 second timeout
164
+ });
165
+ const buffer = Buffer.from(response.data);
166
+ console.log('Downloaded image buffer size:', buffer.length);
167
+
168
+ // Check minimum file size (1KB) to avoid empty/corrupted files
169
+ if (buffer.length < 1024) {
170
+ console.log(`Member ${memberId} has file too small: ${buffer.length} bytes`);
171
+ return {
172
+ success: true,
173
+ message: `File too small (${buffer.length} bytes) - skipped`,
174
+ };
175
+ }
176
+
177
+ const contentType = response.headers['content-type'];
178
+
179
+ // Validate content type after download
180
+ if (!isValidContentType(contentType)) {
181
+ console.log(`Member ${memberId} has invalid content type: ${contentType}`);
182
+ return {
183
+ success: true,
184
+ message: `Invalid content type: ${contentType} - skipped`,
185
+ };
186
+ }
187
+
188
+ // Determine file extension from content type
189
+ const extension = contentType.includes('png')
190
+ ? 'png'
191
+ : contentType.includes('gif')
192
+ ? 'gif'
193
+ : contentType.includes('webp')
194
+ ? 'webp'
195
+ : contentType.includes('bmp')
196
+ ? 'bmp'
197
+ : 'jpg';
198
+
199
+ // Double-check: ensure we're not trying to upload non-image files
200
+ const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
201
+ if (!allowedExtensions.includes(extension)) {
202
+ console.log(`Member ${memberId} has invalid file extension: ${extension}`);
203
+ return {
204
+ success: true,
205
+ message: `Invalid file extension: ${extension} - skipped`,
206
+ };
207
+ }
208
+
209
+ const sanitizedFileName = `profile-${memberId}-${Date.now()}.${extension}`.replace(/\./g, '_');
210
+ const uploadUrl = (
211
+ await files.generateFileUploadUrl(contentType, {
212
+ fileName: sanitizedFileName,
213
+ filePath: 'member-profiles',
214
+ })
215
+ ).uploadUrl;
216
+ const params = { filename: sanitizedFileName };
217
+ const headers = {
218
+ 'Content-Type': contentType,
219
+ };
220
+
221
+ const uploadResponse = await axios.put(uploadUrl, buffer, {
222
+ headers,
223
+ params,
224
+ });
225
+ const fileUrl = uploadResponse.data.file.url;
226
+ const updatedMember = {
227
+ ...member,
228
+ profileImage: fileUrl,
229
+ };
230
+ await updateMember(updatedMember);
231
+
232
+ return {
233
+ success: true,
234
+ message: 'Profile image updated successfully',
235
+ oldUrl: member.profileImage,
236
+ newUrl: fileUrl,
237
+ };
238
+ } catch (error) {
239
+ console.error(`Error updating profile image for member ${memberId}:`, error);
240
+
241
+ // Handle specific HTTP errors
242
+ if (error.response) {
243
+ const status = error.response.status;
244
+ if (status === 403) {
245
+ return {
246
+ success: true,
247
+ message: `403 Forbidden - Access denied to image URL - skipped`,
248
+ };
249
+ } else if (status === 404) {
250
+ return {
251
+ success: true,
252
+ message: `404 Not Found - Image URL not found - skipped`,
253
+ };
254
+ } else if (status === 406) {
255
+ return {
256
+ success: true,
257
+ message: `406 Not Acceptable - Server rejected request headers - skipped`,
258
+ };
259
+ } else if (status >= 400 && status < 500) {
260
+ return {
261
+ success: true,
262
+ message: `${status} Client Error - Invalid image URL - skipped`,
263
+ };
264
+ }
265
+ }
266
+
267
+ return {
268
+ success: false,
269
+ error: error.message || error,
270
+ };
271
+ }
272
+ }
273
+
274
+ async function getAWSTokens() {
275
+ const [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY] = await Promise.all([
276
+ getSecret('AWS_ACCESS_KEY_ID'),
277
+ getSecret('AWS_SECRET_ACCESS_KEY'),
278
+ ]);
279
+
280
+ // const AWS_SESSION_TOKEN = await getSecret("AWS_SESSION_TOKEN")
281
+ return {
282
+ AWS_ACCESS_KEY_ID,
283
+ AWS_SECRET_ACCESS_KEY,
284
+ };
285
+ }
286
+
287
+ async function generateSitemapXml(members) {
288
+ const baseUrl = await getSiteBaseUrl();
289
+ const profilePageUrl = `${baseUrl}/${PAGES_PATHS.PROFILE}`;
290
+ const urls = members
291
+ .map(m => {
292
+ const loc = `${profilePageUrl}/${encodeURIComponent(m.url)}`;
293
+ const lastmod =
294
+ m && m._updatedDate
295
+ ? `\n <lastmod>${encodeXml(formatDateOnly(m._updatedDate))}</lastmod>`
296
+ : '';
297
+ return ` <url>\n <loc>${encodeXml(loc)}</loc>${lastmod}\n </url>`;
298
+ })
299
+ .join('\n');
300
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
301
+ }
302
+ async function uploadMembersSitemap({ members, tokens, destinationFileName, siteAssociation }) {
303
+ const toLowerCaseSiteAssociation = siteAssociation.toLowerCase().trim();
304
+ const bucketHostname = (bucket, region) => {
305
+ const r = region || process?.env?.AWS_REGION || process?.env?.AWS_DEFAULT_REGION || 'us-east-1';
306
+ return `${bucket}.s3.${r}.amazonaws.com`;
307
+ };
308
+ if (!siteAssociation) {
309
+ throw new Error('Site association is required to determine the AWS S3 bucket name');
310
+ }
311
+ const bucket = `${toLowerCaseSiteAssociation}-sitemap`; // e.g: 'abmp-sitemap' or 'ascp-sitemap'
312
+ const region = 'us-east-1';
313
+ const destination_file_name = destinationFileName;
314
+
315
+ console.log('Sitemap generation started');
316
+ const xml = await generateSitemapXml(members);
317
+ console.log('Sitemap generation completed');
318
+ const body = xml;
319
+ console.log('Body length:', body.length);
320
+ const sha256Hex = crypto.createHash('sha256').update(body).digest('hex');
321
+ console.log('SHA256 hash calculated');
322
+ const host = bucketHostname(bucket, region);
323
+ const method = 'PUT';
324
+ const pathName = `/${encodeURI(destination_file_name).replace(/%2F/g, '/')}`;
325
+ console.log('Path name calculated');
326
+ const headers = {
327
+ 'Content-Type': 'application/xml',
328
+ 'X-Amz-Content-Sha256': sha256Hex,
329
+ };
330
+
331
+ const creds = {
332
+ accessKeyId: tokens.AWS_ACCESS_KEY_ID,
333
+ secretAccessKey: tokens.AWS_SECRET_ACCESS_KEY,
334
+ // sessionToken: tokens.AWS_SESSION_TOKEN,
335
+ };
336
+
337
+ const reqOpts = {
338
+ host,
339
+ path: pathName,
340
+ service: 's3',
341
+ region: region,
342
+ method,
343
+ headers,
344
+ body,
345
+ };
346
+ aws4.sign(reqOpts, creds);
347
+ console.log('Request options signed');
348
+
349
+ const url = `https://${host}${pathName}`;
350
+ console.log('url', url);
351
+ const res = await fetch(url, { method, headers: reqOpts.headers, body });
352
+ if (!res.ok) {
353
+ const respText = await res.text();
354
+ console.log('Response body', respText);
355
+ throw new Error(`S3 PUT failed ${res.status} ${res.statusText}: ${respText}`);
356
+ }
357
+ }
358
+
359
+ async function stsPost(body, baseAccessKeyId, baseSecretAccessKey) {
360
+ const host = 'sts.amazonaws.com';
361
+ const method = 'POST';
362
+ const path = '/';
363
+ const headers = {
364
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
365
+ 'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD',
366
+ };
367
+ const reqOpts = {
368
+ host,
369
+ path,
370
+ service: 'sts',
371
+ region: 'us-east-1',
372
+ method,
373
+ headers,
374
+ body,
375
+ };
376
+ const parseXmlVal = (xml, tag) => {
377
+ const m = xml.match(new RegExp(`<${tag}>([^<]+)</${tag}>`));
378
+ return m ? m[1] : '';
379
+ };
380
+ aws4.sign(reqOpts, {
381
+ accessKeyId: baseAccessKeyId,
382
+ secretAccessKey: baseSecretAccessKey,
383
+ });
384
+ const res = await fetch(`https://${host}${path}`, {
385
+ method,
386
+ headers: reqOpts.headers,
387
+ body,
388
+ });
389
+ const text = await res.text();
390
+ if (!res.ok) throw new Error(`STS ${res.status}: ${text}`);
391
+
392
+ const accessKeyId = parseXmlVal(text, 'AccessKeyId');
393
+ const secretAccessKey = parseXmlVal(text, 'SecretAccessKey');
394
+ const sessionToken = parseXmlVal(text, 'SessionToken');
395
+ const expiration = parseXmlVal(text, 'Expiration');
396
+ if (!accessKeyId || !secretAccessKey || !sessionToken || !expiration) {
397
+ throw new Error('Failed parsing STS response');
398
+ }
399
+ return {
400
+ accessKeyId,
401
+ secretAccessKey,
402
+ // sessionToken,
403
+ expiresAt: new Date(expiration).toISOString(),
404
+ };
405
+ }
406
+
407
+ // GetSessionToken (no role)
408
+ function getNewStsSessionToken(baseAccessKeyId, baseSecretAccessKey, durationSeconds = 3600) {
409
+ const body = `Action=GetSessionToken&Version=2011-06-15&DurationSeconds=${durationSeconds}`;
410
+ return stsPost(body, baseAccessKeyId, baseSecretAccessKey);
411
+ }
412
+
413
+ module.exports = {
414
+ updateMemberRichContent,
415
+ updateMemberProfileImage,
416
+ getAWSTokens,
417
+ uploadMembersSitemap,
418
+ getNewStsSessionToken, //Dev only Method
419
+ };