abmp-npm 1.1.64 → 1.1.66
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/backend/consts.js +2 -0
- package/backend/daily-pull/sync-to-cms-methods.js +1 -1
- package/backend/index.js +2 -1
- package/backend/login/index.js +19 -0
- package/backend/login/qa-login-methods.js +72 -0
- package/backend/login/sso-methods.js +158 -0
- package/backend/members-data-methods.js +94 -1
- package/backend/routers/index.js +3 -0
- package/backend/routers/methods.js +177 -0
- package/backend/routers/utils.js +118 -0
- package/backend/utils.js +40 -39
- package/package.json +8 -2
- package/pages/LoadingPage.js +20 -0
- package/pages/QAPage.js +39 -0
- package/pages/index.js +2 -0
- package/public/Utils/sharedUtils.js +1 -0
- package/public/consts.js +1 -0
- package/public/index.js +1 -0
- package/public/sso-auth-methods.js +43 -0
- package/backend/sso-methods.js +0 -88
package/backend/consts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const PAC_API_URL = 'https://members.abmp.com/eweb/api/Wix';
|
|
2
|
+
const SSO_TOKEN_AUTH_API_URL = 'https://members.professionalassistcorp.com/';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Valid configuration keys for getSiteConfigs function
|
|
@@ -38,4 +39,5 @@ module.exports = {
|
|
|
38
39
|
PAC_API_URL,
|
|
39
40
|
COMPILED_FILTERS_FIELDS,
|
|
40
41
|
MEMBERSHIPS_TYPES,
|
|
42
|
+
SSO_TOKEN_AUTH_API_URL,
|
|
41
43
|
};
|
|
@@ -2,7 +2,7 @@ const { taskManager } = require('psdev-task-manager');
|
|
|
2
2
|
|
|
3
3
|
const { CONFIG_KEYS } = require('../consts');
|
|
4
4
|
const { fetchPACMembers } = require('../pac-api-methods');
|
|
5
|
-
const { TASKS_NAMES } = require('../tasks');
|
|
5
|
+
const { TASKS_NAMES } = require('../tasks/consts');
|
|
6
6
|
const { getSiteConfigs } = require('../utils');
|
|
7
7
|
|
|
8
8
|
const { bulkProcessAndSaveMemberData } = require('./bulk-process-methods');
|
package/backend/index.js
CHANGED
|
@@ -9,7 +9,8 @@ module.exports = {
|
|
|
9
9
|
...require('./members-area-methods'), //TODO: remove it once we finish NPM movement
|
|
10
10
|
...require('./members-data-methods'), //TODO: remove it once we finish NPM movement
|
|
11
11
|
...require('./cms-data-methods'), //TODO: remove it once we finish NPM movement
|
|
12
|
-
...require('./
|
|
12
|
+
...require('./routers'),
|
|
13
|
+
...require('./login'),
|
|
13
14
|
...require('./data-hooks'),
|
|
14
15
|
...require('./http-functions'),
|
|
15
16
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { loginQAMember } = require('./qa-login-methods');
|
|
2
|
+
const { authenticateSSOToken } = require('./sso-methods');
|
|
3
|
+
|
|
4
|
+
const createLoginMethods = generateSessionToken => {
|
|
5
|
+
//There is no generateSessionToken SDK version, and the signOn of @wix/identity returns 403 error regardless that the permissions are valid
|
|
6
|
+
//Therefore, as a workaround we need to inject the Velo version of generateSessionToken to the login methods.
|
|
7
|
+
const injectGenerateSessionTokenToMethod =
|
|
8
|
+
method =>
|
|
9
|
+
async (...args) =>
|
|
10
|
+
await method(...args, generateSessionToken);
|
|
11
|
+
return {
|
|
12
|
+
loginQAMember: injectGenerateSessionTokenToMethod(loginQAMember),
|
|
13
|
+
authenticateSSOToken: injectGenerateSessionTokenToMethod(authenticateSSOToken),
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
createLoginMethods,
|
|
19
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { getMemberByEmail, getQAUsers } = require('../members-data-methods');
|
|
2
|
+
const { getSecret } = require('../utils');
|
|
3
|
+
|
|
4
|
+
const validateQAUser = async userEmail => {
|
|
5
|
+
const qaUsers = await getQAUsers();
|
|
6
|
+
const matchingUser = qaUsers.find(user => user.email === userEmail);
|
|
7
|
+
if (!matchingUser) {
|
|
8
|
+
return { error: `Invalid user email: ${userEmail}` };
|
|
9
|
+
}
|
|
10
|
+
return { valid: true, user: matchingUser };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Login a QA user
|
|
15
|
+
* @param {Object} params - The parameters for the login
|
|
16
|
+
* @param {string} params.userEmail - The email of the user to login
|
|
17
|
+
* @param {string} params.secret - The secret of the user to login
|
|
18
|
+
* @param {Function} generateSessionToken - a dependency of the method, injected by the createLoginMethods function
|
|
19
|
+
* @returns {Promise<Object>} The result of the login
|
|
20
|
+
*/
|
|
21
|
+
const loginQAMember = async ({ userEmail, secret }, generateSessionToken) => {
|
|
22
|
+
try {
|
|
23
|
+
const userValidation = await validateQAUser(userEmail);
|
|
24
|
+
if (userValidation.error) {
|
|
25
|
+
return { success: false, error: userValidation.error };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const qaSecret = await getSecret('ABMP_QA_SECRET');
|
|
29
|
+
if (secret !== qaSecret) {
|
|
30
|
+
return { success: false, error: 'Invalid secret' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const token = await generateSessionToken(userValidation.user, qaSecret);
|
|
34
|
+
|
|
35
|
+
const result = await getMemberCMSId(userEmail);
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
return { success: false, error: result.error };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
token,
|
|
43
|
+
memberCMSId: result.memberCMSId,
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('QA login error:', error);
|
|
47
|
+
return { error: 'Failed to generate session token' };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function getMemberCMSId(userEmail) {
|
|
52
|
+
try {
|
|
53
|
+
const userValidation = await validateQAUser(userEmail);
|
|
54
|
+
if (userValidation.error) {
|
|
55
|
+
return { success: false, error: userValidation.error };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const member = await getMemberByEmail(userEmail);
|
|
59
|
+
|
|
60
|
+
if (!member) {
|
|
61
|
+
return { success: false, error: `No Member found in DB matching email: ${userEmail}` };
|
|
62
|
+
}
|
|
63
|
+
return { success: true, memberCMSId: member._id };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error getting member CMS ID:', error);
|
|
66
|
+
return { success: false, error: 'Failed to retrieve member data' };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
loginQAMember,
|
|
72
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const { createHmac } = require('crypto');
|
|
2
|
+
|
|
3
|
+
const { decode } = require('jwt-js-decode');
|
|
4
|
+
|
|
5
|
+
const { CONFIG_KEYS, SSO_TOKEN_AUTH_API_URL } = require('../consts');
|
|
6
|
+
const { MEMBER_ACTIONS } = require('../daily-pull/consts');
|
|
7
|
+
const { getCurrentMember } = require('../members-area-methods');
|
|
8
|
+
const { getMemberByContactId, getSiteMemberId } = require('../members-data-methods');
|
|
9
|
+
const {
|
|
10
|
+
formatDateToMonthYear,
|
|
11
|
+
getAddressDisplayOptions,
|
|
12
|
+
isStudent,
|
|
13
|
+
getSiteConfigs,
|
|
14
|
+
getSecret,
|
|
15
|
+
} = require('../utils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates member token and retrieves member data
|
|
19
|
+
* @param {string} memberIdInput - The member ID to validate
|
|
20
|
+
* @returns {Promise<{memberData: Object|null, isValid: boolean}>} Validation result with member data
|
|
21
|
+
*/
|
|
22
|
+
async function validateMemberToken(memberIdInput) {
|
|
23
|
+
const invalidTokenResponse = { memberData: null, isValid: false };
|
|
24
|
+
|
|
25
|
+
if (!memberIdInput) {
|
|
26
|
+
return invalidTokenResponse;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const member = await getCurrentMember();
|
|
31
|
+
if (!member || !member._id) {
|
|
32
|
+
console.log(
|
|
33
|
+
'member not found from members.getCurrentMember() for memberIdInput',
|
|
34
|
+
memberIdInput
|
|
35
|
+
);
|
|
36
|
+
return invalidTokenResponse;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const [dbMember, siteConfigs] = await Promise.all([
|
|
40
|
+
getMemberByContactId(member._id),
|
|
41
|
+
getSiteConfigs(),
|
|
42
|
+
]);
|
|
43
|
+
const siteAssociation = siteConfigs[CONFIG_KEYS.SITE_ASSOCIATION];
|
|
44
|
+
const membersExternalPortalUrl = siteConfigs[CONFIG_KEYS.MEMBERS_EXTERNAL_PORTAL_URL];
|
|
45
|
+
console.log('dbMember by contact id is:', dbMember);
|
|
46
|
+
console.log('member._id', member._id);
|
|
47
|
+
|
|
48
|
+
if (!dbMember?._id) {
|
|
49
|
+
const errorMessage = `No record found in DB for logged in Member [Corrupted Data - Duplicate Members? ] - There is no match in DB for currentMember: ${JSON.stringify(
|
|
50
|
+
{ memberIdInput, currentMemberId: member._id }
|
|
51
|
+
)}`;
|
|
52
|
+
console.error(errorMessage);
|
|
53
|
+
throw new Error('CORRUPTED_MEMBER_DATA');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`Id found in DB for memberIdInput :${memberIdInput} is ${dbMember?._id}`);
|
|
57
|
+
|
|
58
|
+
const memberData = dbMember;
|
|
59
|
+
|
|
60
|
+
// Format membership dates
|
|
61
|
+
memberData.memberships = memberData.memberships.map(membership => ({
|
|
62
|
+
...membership,
|
|
63
|
+
membersince: formatDateToMonthYear(membership.membersince),
|
|
64
|
+
isSiteAssociation: membership.association === siteAssociation,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const savedMemberId = memberData?._id;
|
|
68
|
+
const isValid = savedMemberId === memberIdInput;
|
|
69
|
+
|
|
70
|
+
if (!savedMemberId || !isValid) {
|
|
71
|
+
return invalidTokenResponse;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if member is dropped
|
|
75
|
+
if (memberData.action === MEMBER_ACTIONS.DROP) {
|
|
76
|
+
return invalidTokenResponse;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add computed properties
|
|
80
|
+
memberData.addressDisplayOption = getAddressDisplayOptions(memberData);
|
|
81
|
+
console.log('memberData', memberData);
|
|
82
|
+
memberData.isStudent = isStudent(memberData);
|
|
83
|
+
|
|
84
|
+
return { memberData, isValid, membersExternalPortalUrl };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Error in validateMemberToken:', error);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function checkAndFetchSSO(token) {
|
|
91
|
+
const SSO_TOKEN_AUTH_API_KEY = await getSecret('SSO_TOKEN_AUTH_API_KEY');
|
|
92
|
+
const signature = createHmac('sha256', SSO_TOKEN_AUTH_API_KEY).update(token).digest('hex');
|
|
93
|
+
const professionalassistcorpUrl = `${SSO_TOKEN_AUTH_API_URL}/eweb/SSOToken.ashx?token=${token}&Partner=Wix&Signature=${signature}`;
|
|
94
|
+
const options = {
|
|
95
|
+
method: 'get',
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
const httpResponse = await fetch(professionalassistcorpUrl, options);
|
|
99
|
+
console.log('httpResponse status', httpResponse.status);
|
|
100
|
+
if (!httpResponse.ok) {
|
|
101
|
+
throw new Error('Fetch did not succeed with status: ' + httpResponse.status);
|
|
102
|
+
}
|
|
103
|
+
const responseToken = await httpResponse.text();
|
|
104
|
+
return responseToken;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error in checkAndFetchSSO', error);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Authenticate an SSO token
|
|
113
|
+
* @param {Object} params - The parameters for the authentication
|
|
114
|
+
* @param {string} params.token - The token to authenticate
|
|
115
|
+
* @param {Function} generateSessionToken - a dependency of the method, injected by the createLoginMethods function
|
|
116
|
+
* @returns {Promise<Object>} The result of the authentication
|
|
117
|
+
*/
|
|
118
|
+
const authenticateSSOToken = async ({ token }, generateSessionToken) => {
|
|
119
|
+
const responseToken = await checkAndFetchSSO(token);
|
|
120
|
+
const isValidToken = Boolean(
|
|
121
|
+
responseToken && typeof responseToken === 'string' && responseToken?.trim()
|
|
122
|
+
);
|
|
123
|
+
const toLogTokenData = {
|
|
124
|
+
isValidToken,
|
|
125
|
+
tokenData: responseToken
|
|
126
|
+
? {
|
|
127
|
+
length: responseToken.length,
|
|
128
|
+
preview: responseToken.substring(0, 50),
|
|
129
|
+
}
|
|
130
|
+
: 'No token',
|
|
131
|
+
};
|
|
132
|
+
console.log('checkAndFetchSSO responseToken data', JSON.stringify(toLogTokenData, null, 2));
|
|
133
|
+
if (isValidToken) {
|
|
134
|
+
const jwt = decode(responseToken);
|
|
135
|
+
const payload = jwt.payload;
|
|
136
|
+
const membersData = await getSiteMemberId(payload);
|
|
137
|
+
console.log('membersDataCollectionId', membersData._id);
|
|
138
|
+
const sessionToken = await generateSessionToken(membersData.email);
|
|
139
|
+
const authObj = {
|
|
140
|
+
type: 'success',
|
|
141
|
+
memberId: membersData._id,
|
|
142
|
+
sessionToken,
|
|
143
|
+
};
|
|
144
|
+
return authObj;
|
|
145
|
+
} else {
|
|
146
|
+
console.log('invalid Token responseToken is: ', responseToken);
|
|
147
|
+
return {
|
|
148
|
+
type: 'error',
|
|
149
|
+
memberId: '',
|
|
150
|
+
sessionToken: '',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
validateMemberToken,
|
|
157
|
+
authenticateSSOToken,
|
|
158
|
+
};
|
|
@@ -2,7 +2,7 @@ const { COLLECTIONS } = require('../public/consts');
|
|
|
2
2
|
|
|
3
3
|
const { MEMBERSHIPS_TYPES } = require('./consts');
|
|
4
4
|
const { updateMemberContactInfo } = require('./contacts-methods');
|
|
5
|
-
const { MEMBER_ACTIONS } = require('./daily-pull');
|
|
5
|
+
const { MEMBER_ACTIONS } = require('./daily-pull/consts');
|
|
6
6
|
const { wixData } = require('./elevated-modules');
|
|
7
7
|
const { createSiteMember } = require('./members-area-methods');
|
|
8
8
|
const {
|
|
@@ -296,6 +296,27 @@ async function urlExists(url, excludeMemberId) {
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Checks URL uniqueness for a member
|
|
301
|
+
* @param {string} url - The URL to check
|
|
302
|
+
* @param {string} memberId - The member ID to exclude from the check
|
|
303
|
+
* @returns {Promise<Object>} Result object with isUnique boolean
|
|
304
|
+
*/
|
|
305
|
+
async function checkUrlUniqueness(url, memberId) {
|
|
306
|
+
if (!url || !memberId) {
|
|
307
|
+
throw new Error('Missing required parameters: url and memberId are required');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const trimmedUrl = url.trim();
|
|
312
|
+
const exists = await urlExists(trimmedUrl, memberId);
|
|
313
|
+
|
|
314
|
+
return { isUnique: !exists };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('Error checking URL uniqueness:', error);
|
|
317
|
+
throw new Error(`Failed to check URL uniqueness: ${error.message}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
299
320
|
/**
|
|
300
321
|
* Get all members with external profile images
|
|
301
322
|
* @returns {Promise<Array>} - Array of member IDs
|
|
@@ -400,6 +421,74 @@ const getMembersByIds = async memberIds => {
|
|
|
400
421
|
}
|
|
401
422
|
};
|
|
402
423
|
|
|
424
|
+
const getMemberByEmail = async email => {
|
|
425
|
+
try {
|
|
426
|
+
const members = await wixData
|
|
427
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
428
|
+
.eq('email', email)
|
|
429
|
+
.limit(2)
|
|
430
|
+
.find()
|
|
431
|
+
.then(res => res.items);
|
|
432
|
+
if (members.length > 1) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`[getMemberByEmail] Multiple members found with email ${email} membersIds are : [${members.map(member => member.memberId).join(', ')}]`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return members[0] || null;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('Error getting member by email:', error);
|
|
440
|
+
throw new Error(`Failed to get member by email: ${error.message}`);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const getQAUsers = async () => {
|
|
445
|
+
try {
|
|
446
|
+
return await wixData
|
|
447
|
+
.query(COLLECTIONS.QA_USERS)
|
|
448
|
+
.include('member')
|
|
449
|
+
.find()
|
|
450
|
+
.then(res => res.items.map(item => item.member));
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('Error getting QA users:', error);
|
|
453
|
+
throw new Error(`Failed to get QA users: ${error.message}`);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
async function getSiteMemberId(data) {
|
|
457
|
+
try {
|
|
458
|
+
console.log('data', data);
|
|
459
|
+
const memberId = data?.pac?.cst_recno;
|
|
460
|
+
if (!memberId) {
|
|
461
|
+
const errorMessage = `Member ID is missing in passed data ${JSON.stringify(data)}`;
|
|
462
|
+
console.error(errorMessage);
|
|
463
|
+
throw new Error(errorMessage);
|
|
464
|
+
}
|
|
465
|
+
const queryMemberResult = await wixData
|
|
466
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
467
|
+
.eq('memberId', Number(memberId))
|
|
468
|
+
.find()
|
|
469
|
+
.then(res => res.items);
|
|
470
|
+
if (!queryMemberResult.length || queryMemberResult.length > 1) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Invalid Members count found in DB for email ${data.email} members count is : [${
|
|
473
|
+
queryMemberResult.length
|
|
474
|
+
}] membersIds are : [${queryMemberResult.map(member => member.memberId).join(', ')}]`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
let memberData = queryMemberResult[0];
|
|
478
|
+
console.log('memberData', memberData);
|
|
479
|
+
const isNewUser = !memberData.contactId;
|
|
480
|
+
if (isNewUser) {
|
|
481
|
+
const memberDataWithContactId = await createContactAndMemberIfNew(memberData);
|
|
482
|
+
console.log('memberDataWithContactId', memberDataWithContactId);
|
|
483
|
+
memberData = memberDataWithContactId;
|
|
484
|
+
}
|
|
485
|
+
return memberData;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error('Error in getSiteMemberId', error.message);
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
403
492
|
module.exports = {
|
|
404
493
|
findMemberByWixDataId,
|
|
405
494
|
createContactAndMemberIfNew,
|
|
@@ -415,4 +504,8 @@ module.exports = {
|
|
|
415
504
|
getAllMembersWithoutContactFormEmail,
|
|
416
505
|
getAllUpdatedLoginEmails,
|
|
417
506
|
getMembersByIds,
|
|
507
|
+
getMemberByEmail,
|
|
508
|
+
getQAUsers,
|
|
509
|
+
getSiteMemberId,
|
|
510
|
+
checkUrlUniqueness,
|
|
418
511
|
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const { PAGES_PATHS } = require('../../public/consts');
|
|
2
|
+
//const { fetchAllItemsInParallel } = require('../cms-data-methods'); unused at host site
|
|
3
|
+
const { CONFIG_KEYS } = require('../consts');
|
|
4
|
+
const { getSiteConfigs } = require('../utils');
|
|
5
|
+
|
|
6
|
+
const { generateSEOTitle, stripHtmlTags, getMemberProfileData } = require('./utils');
|
|
7
|
+
|
|
8
|
+
const createRoutersHandlers = wixRouterMethods => {
|
|
9
|
+
const {
|
|
10
|
+
redirect,
|
|
11
|
+
ok,
|
|
12
|
+
notFound,
|
|
13
|
+
sendStatus,
|
|
14
|
+
WixRouterSitemapEntry: _WixRouterSitemapEntry,
|
|
15
|
+
} = wixRouterMethods; // These dependencies needs to be injected as they do not have an SDK equivalent for now
|
|
16
|
+
|
|
17
|
+
async function profileRouter(request) {
|
|
18
|
+
const slug = request.path[0];
|
|
19
|
+
if (!slug) {
|
|
20
|
+
return redirect(request.baseUrl);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const siteConfigs = await getSiteConfigs();
|
|
24
|
+
const siteAssociation = siteConfigs[CONFIG_KEYS.SITE_ASSOCIATION];
|
|
25
|
+
const defaultSEODescription = siteConfigs[CONFIG_KEYS.DEFAULT_PROFILE_SEO_DESCRIPTION];
|
|
26
|
+
const siteLogoUrl = siteConfigs[CONFIG_KEYS.SITE_LOGO_URL];
|
|
27
|
+
const defaultProfileImage = siteConfigs[CONFIG_KEYS.DEFAULT_PROFILE_IMAGE];
|
|
28
|
+
const profileData = await getMemberProfileData(slug, siteAssociation);
|
|
29
|
+
if (profileData && profileData.showWixUrl) {
|
|
30
|
+
const ogImage = profileData.profileImage || profileData.logoImage || siteLogoUrl;
|
|
31
|
+
const seoTitle = generateSEOTitle({
|
|
32
|
+
fullName: profileData.fullName,
|
|
33
|
+
areasOfPractices: profileData.areasOfPractices,
|
|
34
|
+
siteAssociation,
|
|
35
|
+
});
|
|
36
|
+
// Use stripped HTML from aboutService rich text content
|
|
37
|
+
let description = stripHtmlTags(profileData.aboutService) || defaultSEODescription;
|
|
38
|
+
|
|
39
|
+
// Limit description to 160 characters for optimal SEO
|
|
40
|
+
if (description.length > 160) {
|
|
41
|
+
description = description.substring(0, 157) + '...';
|
|
42
|
+
}
|
|
43
|
+
const profileUrl = `${request.baseUrl}/${PAGES_PATHS.PROFILE}/${profileData.url}`;
|
|
44
|
+
const isPrivateMember = profileData.isPrivateMember;
|
|
45
|
+
const seoData = {
|
|
46
|
+
title: seoTitle,
|
|
47
|
+
description: description,
|
|
48
|
+
noIndex: isPrivateMember,
|
|
49
|
+
metaTags: [
|
|
50
|
+
{
|
|
51
|
+
name: 'description',
|
|
52
|
+
content: description,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'keywords',
|
|
56
|
+
content:
|
|
57
|
+
`${profileData.fullName}, ${profileData.areasOfPractices ? profileData.areasOfPractices.slice(0, 3).join(', ') : ''}, ${siteAssociation}, ${profileData.city || ''}, ${profileData.state || ''}`
|
|
58
|
+
.replace(/,\s*,/g, ',')
|
|
59
|
+
.replace(/^,|,$/g, ''),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'author',
|
|
63
|
+
content: profileData.fullName,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'robots',
|
|
67
|
+
content: isPrivateMember ? 'noindex, nofollow' : 'index, follow',
|
|
68
|
+
},
|
|
69
|
+
// Open Graph tags
|
|
70
|
+
{
|
|
71
|
+
property: 'og:type',
|
|
72
|
+
content: 'profile',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
property: 'og:title',
|
|
76
|
+
content: seoTitle,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
property: 'og:description',
|
|
80
|
+
content: description,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
property: 'og:image',
|
|
84
|
+
content: ogImage,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
property: 'og:url',
|
|
88
|
+
content: profileUrl,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
property: 'og:site_name',
|
|
92
|
+
content: `${siteAssociation} Members`,
|
|
93
|
+
},
|
|
94
|
+
// Twitter Card tags
|
|
95
|
+
{
|
|
96
|
+
name: 'twitter:card',
|
|
97
|
+
content: 'summary_large_image',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'twitter:title',
|
|
101
|
+
content: seoTitle,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'twitter:description',
|
|
105
|
+
content: description,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'twitter:image',
|
|
109
|
+
content: ogImage,
|
|
110
|
+
},
|
|
111
|
+
// Additional SEO tags
|
|
112
|
+
{
|
|
113
|
+
name: 'geo.region',
|
|
114
|
+
content: profileData.state || '',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'geo.placename',
|
|
118
|
+
content: profileData.city || '',
|
|
119
|
+
},
|
|
120
|
+
].filter(tag => tag.content && tag.content.trim() !== ''), // Remove empty tags
|
|
121
|
+
};
|
|
122
|
+
return ok('profile', { ...profileData, defaultProfileImage }, seoData);
|
|
123
|
+
}
|
|
124
|
+
return notFound();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(error);
|
|
127
|
+
return sendStatus('500', 'Internal Server Error');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function profileSiteMap(_sitemapRequest) {
|
|
131
|
+
return [];
|
|
132
|
+
// Commented out - currently disabled in host site
|
|
133
|
+
// try {
|
|
134
|
+
// const membersQuery = wixData
|
|
135
|
+
// .query(COLLECTIONS.MEMBERS_DATA)
|
|
136
|
+
// .eq('showWixUrl', true)
|
|
137
|
+
// .isNotEmpty('url')
|
|
138
|
+
// .ne('action', 'drop')
|
|
139
|
+
// .fields('url', 'fullName');
|
|
140
|
+
|
|
141
|
+
// const allMembers = await fetchAllItemsInParallel(membersQuery);
|
|
142
|
+
|
|
143
|
+
// const batchSize = 1000;
|
|
144
|
+
// const sitemapEntries = [];
|
|
145
|
+
// const totalItems = allMembers.items.length;
|
|
146
|
+
|
|
147
|
+
// for (let i = 0; i < totalItems; i += batchSize) {
|
|
148
|
+
// const batch = allMembers.items.slice(i, i + batchSize);
|
|
149
|
+
// const batchEntries = batch.map(member => {
|
|
150
|
+
// const entry = new WixRouterSitemapEntry(member.fullName);
|
|
151
|
+
// entry.pageName = 'profile';
|
|
152
|
+
// entry.url = `${PAGES_PATHS.PROFILE}/${member.url}`;
|
|
153
|
+
// entry.title = member.fullName;
|
|
154
|
+
// entry.changeFrequency = 'monthly';
|
|
155
|
+
// entry.priority = 1.0;
|
|
156
|
+
// return entry;
|
|
157
|
+
// });
|
|
158
|
+
// sitemapEntries.push(...batchEntries);
|
|
159
|
+
// }
|
|
160
|
+
|
|
161
|
+
// return sitemapEntries;
|
|
162
|
+
// } catch (error) {
|
|
163
|
+
// console.error('Error generating profile sitemap:', error);
|
|
164
|
+
// return [];
|
|
165
|
+
// }
|
|
166
|
+
}
|
|
167
|
+
//Add other routers here
|
|
168
|
+
return {
|
|
169
|
+
profileRouter,
|
|
170
|
+
profileSiteMap,
|
|
171
|
+
//Add other routers here
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
createRoutersHandlers,
|
|
177
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const { getMainAddress } = require('../../public/Utils/sharedUtils');
|
|
2
|
+
const { getMemberBySlug } = require('../members-data-methods');
|
|
3
|
+
const {
|
|
4
|
+
getAddressesByStatus,
|
|
5
|
+
formatDateToMonthYear,
|
|
6
|
+
hasStudentMembership,
|
|
7
|
+
isPAC_STAFF,
|
|
8
|
+
} = require('../utils');
|
|
9
|
+
|
|
10
|
+
function generateSEOTitle({ fullName, areasOfPractices, siteAssociation }) {
|
|
11
|
+
return `${fullName}${
|
|
12
|
+
areasOfPractices && areasOfPractices.length > 0
|
|
13
|
+
? ` | ${areasOfPractices.slice(0, 3).join(', ')}`
|
|
14
|
+
: ''
|
|
15
|
+
} | ${siteAssociation} Member`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripHtmlTags(html) {
|
|
19
|
+
if (!html) return '';
|
|
20
|
+
// Remove HTML tags and decode HTML entities
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
23
|
+
.replace(/ /g, ' ') // Replace non-breaking spaces
|
|
24
|
+
.replace(/&/g, '&') // Replace encoded ampersands
|
|
25
|
+
.replace(/</g, '<') // Replace encoded less than
|
|
26
|
+
.replace(/>/g, '>') // Replace encoded greater than
|
|
27
|
+
.replace(/"/g, '"') // Replace encoded quotes
|
|
28
|
+
.replace(/'/g, "'") // Replace encoded apostrophes
|
|
29
|
+
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
|
|
30
|
+
.trim(); // Remove leading/trailing whitespace
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldHaveStudentBadge(member, siteAssociation) {
|
|
34
|
+
return hasStudentMembership({
|
|
35
|
+
member,
|
|
36
|
+
checkAssociation: true,
|
|
37
|
+
siteAssociation,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function transformMemberToProfileData(member, siteAssociation) {
|
|
42
|
+
if (!member) {
|
|
43
|
+
throw new Error('member is required');
|
|
44
|
+
}
|
|
45
|
+
const addresses = member.addresses || [];
|
|
46
|
+
const mainAddress = getMainAddress(member.addressDisplayOption, addresses);
|
|
47
|
+
const licenceNo = member.licenses
|
|
48
|
+
?.map(val => val.license)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(', ');
|
|
51
|
+
const processedAddresses = getAddressesByStatus(member.addresses, member.addressDisplayOption);
|
|
52
|
+
|
|
53
|
+
const memberships = member.memberships || [];
|
|
54
|
+
const siteAssociationMembership = memberships.find(m => m.association === siteAssociation);
|
|
55
|
+
|
|
56
|
+
const areasOfPractices =
|
|
57
|
+
member.areasOfPractices
|
|
58
|
+
?.filter(item => typeof item === 'string' && item.trim().length > 0)
|
|
59
|
+
.map(item => item.trim())
|
|
60
|
+
.sort((a, b) =>
|
|
61
|
+
a.localeCompare(b, undefined, {
|
|
62
|
+
sensitivity: 'base',
|
|
63
|
+
numeric: true,
|
|
64
|
+
})
|
|
65
|
+
) || [];
|
|
66
|
+
return {
|
|
67
|
+
mainAddress,
|
|
68
|
+
testimonials: member.testimonial || [],
|
|
69
|
+
licenceNo,
|
|
70
|
+
processedAddresses,
|
|
71
|
+
memberSince:
|
|
72
|
+
(member.showABMP &&
|
|
73
|
+
siteAssociationMembership &&
|
|
74
|
+
formatDateToMonthYear(siteAssociationMembership?.membersince)) ||
|
|
75
|
+
'',
|
|
76
|
+
shouldHaveStudentBadge: shouldHaveStudentBadge(member, siteAssociation),
|
|
77
|
+
logoImage: member.logoImage,
|
|
78
|
+
fullName: member.fullName,
|
|
79
|
+
profileImage: member.profileImage,
|
|
80
|
+
showContactForm: member.showContactForm,
|
|
81
|
+
bookingUrl: member.bookingUrl,
|
|
82
|
+
aboutService: member.aboutService,
|
|
83
|
+
businessName: (member.showBusinessName && member.businessName) || '',
|
|
84
|
+
phone: member.toShowPhone || '',
|
|
85
|
+
areasOfPractices,
|
|
86
|
+
gallery: member.gallery,
|
|
87
|
+
bannerImages: member.bannerImages,
|
|
88
|
+
showWixUrl: member.showWixUrl,
|
|
89
|
+
_id: member._id,
|
|
90
|
+
url: member.url,
|
|
91
|
+
isPrivateMember: isPAC_STAFF(member),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getMemberProfileData = async (slug, siteAssociation) => {
|
|
96
|
+
try {
|
|
97
|
+
const member = await getMemberBySlug({
|
|
98
|
+
slug,
|
|
99
|
+
excludeDropped: true,
|
|
100
|
+
excludeSearchedMember: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!member) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return transformMemberToProfileData(member, siteAssociation);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(error);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
generateSEOTitle,
|
|
116
|
+
stripHtmlTags,
|
|
117
|
+
getMemberProfileData,
|
|
118
|
+
};
|
package/backend/utils.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { auth } = require('@wix/essentials');
|
|
2
2
|
const { secrets } = require('@wix/secrets');
|
|
3
3
|
const { site } = require('@wix/urls');
|
|
4
4
|
const { encode } = require('ngeohash');
|
|
5
5
|
|
|
6
|
-
const { COLLECTIONS } = require('../public/consts');
|
|
6
|
+
const { COLLECTIONS, ADDRESS_STATUS_TYPES } = require('../public/consts');
|
|
7
|
+
const { formatAddress, generateId } = require('../public/Utils/sharedUtils');
|
|
7
8
|
|
|
8
|
-
const { CONFIG_KEYS, GEO_HASH_PRECISION } = require('./consts');
|
|
9
|
+
const { CONFIG_KEYS, GEO_HASH_PRECISION, MEMBERSHIPS_TYPES } = require('./consts');
|
|
9
10
|
const { wixData } = require('./elevated-modules');
|
|
10
|
-
const
|
|
11
|
-
const elevatedGetSecretValue = elevate(secrets.getSecretValue);
|
|
11
|
+
const elevatedGetSecretValue = auth.elevate(secrets.getSecretValue);
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Retrieves site configuration values from the database
|
|
15
|
-
* @param {
|
|
16
|
-
* - 'AUTOMATION_EMAIL_TRIGGER_ID' - Email template ID for triggered emails
|
|
17
|
-
* - 'SITE_ASSOCIATION' - Site association configuration
|
|
15
|
+
* @param {keyof typeof CONFIG_KEYS} [configKey] - The configuration key to retrieve
|
|
18
16
|
* @returns {Promise<any>} The configuration value for the specified key, or all configs if no key provided
|
|
19
17
|
* @example
|
|
20
18
|
* // Get specific config
|
|
@@ -60,18 +58,26 @@ function formatDateToMonthYear(dateString) {
|
|
|
60
58
|
return date.toLocaleDateString('en-US', options);
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
* Check if member is a student
|
|
65
|
-
* @param {Object} member - The member object
|
|
66
|
-
* @returns {boolean} True if member has student membership
|
|
67
|
-
*/
|
|
68
|
-
function isStudent(member) {
|
|
61
|
+
function hasStudentMembership({ member, checkAssociation = false, siteAssociation = null }) {
|
|
69
62
|
const memberships = member?.memberships;
|
|
70
63
|
if (!Array.isArray(memberships)) return false;
|
|
71
64
|
|
|
72
|
-
return memberships.some(membership =>
|
|
65
|
+
return memberships.some(membership => {
|
|
66
|
+
const isStudent = membership.membertype === MEMBERSHIPS_TYPES.STUDENT;
|
|
67
|
+
const hasCorrectAssociation = !checkAssociation || membership.association === siteAssociation;
|
|
68
|
+
return isStudent && hasCorrectAssociation;
|
|
69
|
+
});
|
|
73
70
|
}
|
|
74
71
|
|
|
72
|
+
function isStudent(member) {
|
|
73
|
+
return hasStudentMembership({ member, checkAssociation: false });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isPAC_STAFF(member) {
|
|
77
|
+
return Boolean(
|
|
78
|
+
member?.memberships?.some(membership => membership.membertype === MEMBERSHIPS_TYPES.PAC_STAFF)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
75
81
|
/**
|
|
76
82
|
* Get address display options for member
|
|
77
83
|
* @param {Object} member - The member object
|
|
@@ -85,7 +91,22 @@ function getAddressDisplayOptions(member) {
|
|
|
85
91
|
}
|
|
86
92
|
return displayOptions;
|
|
87
93
|
}
|
|
88
|
-
|
|
94
|
+
function getAddressesByStatus(addresses = [], addressDisplayOption = []) {
|
|
95
|
+
const visible = addresses.filter(addr => addr.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW);
|
|
96
|
+
if (visible.length < 2) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const opts = Array.isArray(addressDisplayOption) ? addressDisplayOption : [];
|
|
100
|
+
const mainOpt = opts.find(o => o.isMain);
|
|
101
|
+
const mainKey = mainOpt ? mainOpt.key : visible[0].key; // fallback to the first visible if none marked
|
|
102
|
+
return visible
|
|
103
|
+
.filter(addr => addr?.key !== mainKey)
|
|
104
|
+
.map(addr => {
|
|
105
|
+
const addressString = formatAddress(addr);
|
|
106
|
+
return addressString ? { _id: generateId(), address: addressString } : null;
|
|
107
|
+
})
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
}
|
|
89
110
|
const queryAllItems = async query => {
|
|
90
111
|
console.log('start query');
|
|
91
112
|
let oldResults = await query.find();
|
|
@@ -132,28 +153,6 @@ const normalizeUrlForComparison = url => {
|
|
|
132
153
|
return url.toLowerCase().replace(/-\d+$/, '');
|
|
133
154
|
};
|
|
134
155
|
|
|
135
|
-
/**
|
|
136
|
-
* Checks URL uniqueness for a member
|
|
137
|
-
* @param {string} url - The URL to check
|
|
138
|
-
* @param {string} memberId - The member ID to exclude from the check
|
|
139
|
-
* @returns {Promise<Object>} Result object with isUnique boolean
|
|
140
|
-
*/
|
|
141
|
-
async function checkUrlUniqueness(url, memberId) {
|
|
142
|
-
if (!url || !memberId) {
|
|
143
|
-
throw new Error('Missing required parameters: url and memberId are required');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const trimmedUrl = url.trim();
|
|
148
|
-
const exists = await urlExists(trimmedUrl, memberId);
|
|
149
|
-
|
|
150
|
-
return { isUnique: !exists };
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error('Error checking URL uniqueness:', error);
|
|
153
|
-
throw new Error(`Failed to check URL uniqueness: ${error.message}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
156
|
async function getSecret(secretKey) {
|
|
158
157
|
return await elevatedGetSecretValue(secretKey).value;
|
|
159
158
|
}
|
|
@@ -197,12 +196,14 @@ module.exports = {
|
|
|
197
196
|
isValidArray,
|
|
198
197
|
normalizeUrlForComparison,
|
|
199
198
|
queryAllItems,
|
|
200
|
-
checkUrlUniqueness,
|
|
201
199
|
formatDateToMonthYear,
|
|
202
200
|
isStudent,
|
|
201
|
+
hasStudentMembership,
|
|
203
202
|
getAddressDisplayOptions,
|
|
204
203
|
getSecret,
|
|
205
204
|
getSiteBaseUrl,
|
|
206
205
|
encodeXml,
|
|
207
206
|
formatDateOnly,
|
|
207
|
+
getAddressesByStatus,
|
|
208
|
+
isPAC_STAFF,
|
|
208
209
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abmp-npm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.66",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
|
+
"check-cycles": "madge --circular .",
|
|
6
7
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
-
"lint": "eslint .",
|
|
8
|
+
"lint": "npm run check-cycles && eslint .",
|
|
8
9
|
"lint:fix": "eslint . --fix",
|
|
9
10
|
"format": "prettier --write \"**/*.{js,json,md}\"",
|
|
10
11
|
"format:check": "prettier --check \"**/*.{js,json,md}\"",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"eslint-plugin-promise": "^7.1.0",
|
|
24
25
|
"globals": "^15.10.0",
|
|
25
26
|
"husky": "^9.1.6",
|
|
27
|
+
"madge": "^8.0.0",
|
|
26
28
|
"prettier": "^3.3.3"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
@@ -30,15 +32,19 @@
|
|
|
30
32
|
"@wix/crm": "^1.0.1061",
|
|
31
33
|
"@wix/data": "^1.0.303",
|
|
32
34
|
"@wix/essentials": "^0.1.28",
|
|
35
|
+
"@wix/identity": "^1.0.178",
|
|
33
36
|
"@wix/media": "^1.0.213",
|
|
34
37
|
"@wix/members": "^1.0.365",
|
|
35
38
|
"@wix/secrets": "^1.0.62",
|
|
36
39
|
"@wix/site-location": "^1.31.0",
|
|
40
|
+
"@wix/site-members": "^1.32.0",
|
|
41
|
+
"@wix/site-storage": "^1.22.0",
|
|
37
42
|
"@wix/site-window": "^1.44.0",
|
|
38
43
|
"@wix/urls": "^1.0.57",
|
|
39
44
|
"aws4": "^1.13.2",
|
|
40
45
|
"axios": "^1.13.1",
|
|
41
46
|
"crypto": "^1.0.1",
|
|
47
|
+
"jwt-js-decode": "^1.9.0",
|
|
42
48
|
"lodash": "^4.17.21",
|
|
43
49
|
"ngeohash": "^0.6.3",
|
|
44
50
|
"phone": "^3.1.67",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { window: wixWindow, rendering } = require('@wix/site-window');
|
|
2
|
+
|
|
3
|
+
const { LIGHTBOX_NAMES } = require('../public/consts');
|
|
4
|
+
const { checkAndLogin } = require('../public/sso-auth-methods');
|
|
5
|
+
|
|
6
|
+
async function loadingPageOnReady(authenticateSSOToken) {
|
|
7
|
+
const renderingEnv = await rendering.env();
|
|
8
|
+
//This calls needs to triggered on client side, otherwise PAC API will return 401 error
|
|
9
|
+
if (renderingEnv === 'browser') {
|
|
10
|
+
//Need to pass authenticateSSOToken to checkAndLogin so it will run as a web method not a public one.
|
|
11
|
+
await checkAndLogin(authenticateSSOToken).catch(error => {
|
|
12
|
+
wixWindow.openLightbox(LIGHTBOX_NAMES.LOGIN_ERROR_ALERT);
|
|
13
|
+
console.error(`Something went wrong while logging in: ${error}`);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
loadingPageOnReady,
|
|
20
|
+
};
|
package/pages/QAPage.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { location: wixLocationFrontend } = require('@wix/site-location');
|
|
2
|
+
const { authentication } = require('@wix/site-members');
|
|
3
|
+
const { local } = require('@wix/site-storage');
|
|
4
|
+
|
|
5
|
+
async function qaPageOnReady({ $w: _$w, loginQAMember }) {
|
|
6
|
+
try {
|
|
7
|
+
const { userEmail, secret, redirectTo, ...restQueryParams } = await wixLocationFrontend.query();
|
|
8
|
+
|
|
9
|
+
if (!userEmail || !secret) {
|
|
10
|
+
throw new Error('Missing required parameters: userEmail and/or secret');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = await loginQAMember({ userEmail, secret });
|
|
14
|
+
|
|
15
|
+
if (!result.success || !result.token) {
|
|
16
|
+
throw new Error(result.error || 'Login failed');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await authentication.applySessionToken(result.token);
|
|
20
|
+
console.log('QA user logged in successfully');
|
|
21
|
+
|
|
22
|
+
await local.setItem('memberId', result.memberCMSId);
|
|
23
|
+
const queryParams = new URLSearchParams({ ...restQueryParams, token: result.memberCMSId });
|
|
24
|
+
const redirectUrl = redirectTo ? `/${redirectTo}?${queryParams.toString()}` : '/';
|
|
25
|
+
|
|
26
|
+
await wixLocationFrontend.to(redirectUrl);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('QA login failed:', error);
|
|
29
|
+
|
|
30
|
+
const qaTextElement = _$w('#qaText');
|
|
31
|
+
if (qaTextElement) {
|
|
32
|
+
qaTextElement.text = 'Login failed: ' + (error.message || 'Unknown error');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
qaPageOnReady,
|
|
39
|
+
};
|
package/pages/index.js
CHANGED
package/public/consts.js
CHANGED
package/public/index.js
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { location: wixLocationFrontend } = require('@wix/site-location');
|
|
2
|
+
const { authentication } = require('@wix/site-members');
|
|
3
|
+
const { local } = require('@wix/site-storage');
|
|
4
|
+
|
|
5
|
+
const { PAGES_PATHS } = require('./consts');
|
|
6
|
+
|
|
7
|
+
const checkAndLogin = async authenticateSSOToken => {
|
|
8
|
+
const query = await wixLocationFrontend.query();
|
|
9
|
+
const token = query['token']?.trim();
|
|
10
|
+
try {
|
|
11
|
+
if (token) {
|
|
12
|
+
const authObj = await authenticateSSOToken({ token });
|
|
13
|
+
console.log('authObj', authObj);
|
|
14
|
+
if (authObj.type == 'success') {
|
|
15
|
+
console.log('success');
|
|
16
|
+
await Promise.all([
|
|
17
|
+
authentication.applySessionToken(authObj?.sessionToken),
|
|
18
|
+
local.setItem('memberId', authObj.memberId),
|
|
19
|
+
]);
|
|
20
|
+
console.log('memberId', authObj.memberId);
|
|
21
|
+
const queryParams = {
|
|
22
|
+
...query,
|
|
23
|
+
token: authObj?.memberId,
|
|
24
|
+
};
|
|
25
|
+
const redirectTo = `${PAGES_PATHS.MEMBERS_FORM}?${new URLSearchParams(queryParams).toString()}`;
|
|
26
|
+
await wixLocationFrontend.to(`/${redirectTo}`);
|
|
27
|
+
} else {
|
|
28
|
+
console.error('Something went wrong while logging in');
|
|
29
|
+
throw new Error('Authentication failed - invalid response from server');
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
console.log('checkAndLogin: No token found');
|
|
33
|
+
throw new Error('No authentication token found in URL');
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error in checkAndLogin', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
checkAndLogin,
|
|
43
|
+
};
|
package/backend/sso-methods.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
const { CONFIG_KEYS } = require('./consts');
|
|
2
|
-
const { MEMBER_ACTIONS } = require('./daily-pull');
|
|
3
|
-
const { getCurrentMember } = require('./members-area-methods');
|
|
4
|
-
const { getMemberByContactId } = require('./members-data-methods');
|
|
5
|
-
const {
|
|
6
|
-
formatDateToMonthYear,
|
|
7
|
-
getAddressDisplayOptions,
|
|
8
|
-
isStudent,
|
|
9
|
-
getSiteConfigs,
|
|
10
|
-
} = require('./utils');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Validates member token and retrieves member data
|
|
14
|
-
* @param {string} memberIdInput - The member ID to validate
|
|
15
|
-
* @returns {Promise<{memberData: Object|null, isValid: boolean}>} Validation result with member data
|
|
16
|
-
*/
|
|
17
|
-
async function validateMemberToken(memberIdInput) {
|
|
18
|
-
const invalidTokenResponse = { memberData: null, isValid: false };
|
|
19
|
-
|
|
20
|
-
if (!memberIdInput) {
|
|
21
|
-
return invalidTokenResponse;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const member = await getCurrentMember();
|
|
26
|
-
if (!member || !member._id) {
|
|
27
|
-
console.log(
|
|
28
|
-
'member not found from members.getCurrentMember() for memberIdInput',
|
|
29
|
-
memberIdInput
|
|
30
|
-
);
|
|
31
|
-
return invalidTokenResponse;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const [dbMember, siteConfigs] = await Promise.all([
|
|
35
|
-
getMemberByContactId(member._id),
|
|
36
|
-
getSiteConfigs(),
|
|
37
|
-
]);
|
|
38
|
-
const siteAssociation = siteConfigs[CONFIG_KEYS.SITE_ASSOCIATION];
|
|
39
|
-
const membersExternalPortalUrl = siteConfigs[CONFIG_KEYS.MEMBERS_EXTERNAL_PORTAL_URL];
|
|
40
|
-
console.log('dbMember by contact id is:', dbMember);
|
|
41
|
-
console.log('member._id', member._id);
|
|
42
|
-
|
|
43
|
-
if (!dbMember?._id) {
|
|
44
|
-
const errorMessage = `No record found in DB for logged in Member [Corrupted Data - Duplicate Members? ] - There is no match in DB for currentMember: ${JSON.stringify(
|
|
45
|
-
{ memberIdInput, currentMemberId: member._id }
|
|
46
|
-
)}`;
|
|
47
|
-
console.error(errorMessage);
|
|
48
|
-
throw new Error('CORRUPTED_MEMBER_DATA');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
console.log(`Id found in DB for memberIdInput :${memberIdInput} is ${dbMember?._id}`);
|
|
52
|
-
|
|
53
|
-
const memberData = dbMember;
|
|
54
|
-
|
|
55
|
-
// Format membership dates
|
|
56
|
-
memberData.memberships = memberData.memberships.map(membership => ({
|
|
57
|
-
...membership,
|
|
58
|
-
membersince: formatDateToMonthYear(membership.membersince),
|
|
59
|
-
isSiteAssociation: membership.association === siteAssociation,
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
const savedMemberId = memberData?._id;
|
|
63
|
-
const isValid = savedMemberId === memberIdInput;
|
|
64
|
-
|
|
65
|
-
if (!savedMemberId || !isValid) {
|
|
66
|
-
return invalidTokenResponse;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check if member is dropped
|
|
70
|
-
if (memberData.action === MEMBER_ACTIONS.DROP) {
|
|
71
|
-
return invalidTokenResponse;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Add computed properties
|
|
75
|
-
memberData.addressDisplayOption = getAddressDisplayOptions(memberData);
|
|
76
|
-
console.log('memberData', memberData);
|
|
77
|
-
memberData.isStudent = isStudent(memberData);
|
|
78
|
-
|
|
79
|
-
return { memberData, isValid, membersExternalPortalUrl };
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error('Error in validateMemberToken:', error);
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
module.exports = {
|
|
87
|
-
validateMemberToken,
|
|
88
|
-
};
|