abmp-npm 10.3.16 → 10.3.17
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/association-expiry.js +20 -16
- package/backend/cms-data-methods.js +9 -0
- package/backend/consts.js +7 -0
- package/backend/daily-pull/process-member-methods.js +4 -0
- package/backend/daily-pull/sync-to-cms-methods.js +10 -3
- package/backend/jobs.js +1 -1
- package/backend/login/sso-methods.js +19 -2
- package/backend/members-data-methods.js +12 -3
- package/backend/routers/utils.js +8 -0
- package/backend/tasks/tasks-configs.js +3 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The per-association expiry rule
|
|
2
|
-
//
|
|
1
|
+
// The per-association expiry rule, shared by the sync, the backfill and the read paths so they
|
|
2
|
+
// cannot drift on what "expired" means. Deliberately free of Wix imports.
|
|
3
3
|
|
|
4
4
|
const ASSOCIATION_EXPIRATION_FIELD = 'associationExpiration';
|
|
5
5
|
const ASSOCIATION_TIME_ZONE = 'America/Denver';
|
|
@@ -14,11 +14,8 @@ const EXPIRATION_OUTCOMES = {
|
|
|
14
14
|
UNREADABLE_EXPIRATION: 'unreadableExpiration',
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* those as local time, so the same feed would mean different days depending on where it ran.
|
|
20
|
-
* @returns {Date|null} UTC midnight, or null if absent or unreadable
|
|
21
|
-
*/
|
|
17
|
+
// PAC sends zoneless ISO strings, which `new Date()` would read as local time - the same feed
|
|
18
|
+
// would then mean different days depending on where it ran.
|
|
22
19
|
const parseExpirationToUtcDate = expiration => {
|
|
23
20
|
if (typeof expiration !== 'string') return null;
|
|
24
21
|
|
|
@@ -37,10 +34,8 @@ const parseExpirationToUtcDate = expiration => {
|
|
|
37
34
|
return isRealDate ? parsed : null;
|
|
38
35
|
};
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* report: no entry for this association is a different thing from a malformed date.
|
|
43
|
-
*/
|
|
37
|
+
// outcome explains a null date for the backfill report: no entry for this association is a very
|
|
38
|
+
// different thing from a malformed one.
|
|
44
39
|
const classifyAssociationExpiration = (member, siteAssociation) => {
|
|
45
40
|
if (!siteAssociation) {
|
|
46
41
|
return { date: null, outcome: EXPIRATION_OUTCOMES.NO_SITE_ASSOCIATION };
|
|
@@ -96,11 +91,8 @@ const memberNeedsAssociationExpirationBackfill = (member, siteAssociation) => {
|
|
|
96
91
|
return storedTime !== resolvedTime;
|
|
97
92
|
};
|
|
98
93
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* everyone expiring that day up to seven hours early, every evening.
|
|
102
|
-
* @param {Date} [now] injectable for tests
|
|
103
|
-
*/
|
|
94
|
+
// Today in Denver, where PAC operates. UTC rolls over first, so a UTC-derived today would hide
|
|
95
|
+
// everyone expiring that day up to seven hours early, every evening.
|
|
104
96
|
const getTodayInAssociationTimeZone = (now = new Date()) => {
|
|
105
97
|
try {
|
|
106
98
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
@@ -127,6 +119,17 @@ const getTodayInAssociationTimeZone = (now = new Date()) => {
|
|
|
127
119
|
}
|
|
128
120
|
};
|
|
129
121
|
|
|
122
|
+
// Mirrors the directory query's `.ge()` for a record already in hand, so search, the profile page
|
|
123
|
+
// and the members area cannot disagree. Missing or unreadable is not current, as in the query.
|
|
124
|
+
const isAssociationExpirationCurrent = (member, now) => {
|
|
125
|
+
const stored = member?.[ASSOCIATION_EXPIRATION_FIELD];
|
|
126
|
+
const expiration = stored instanceof Date ? stored : stored ? new Date(stored) : null;
|
|
127
|
+
|
|
128
|
+
if (!expiration || Number.isNaN(expiration.getTime())) return false;
|
|
129
|
+
|
|
130
|
+
return expiration.getTime() >= getTodayInAssociationTimeZone(now).getTime();
|
|
131
|
+
};
|
|
132
|
+
|
|
130
133
|
module.exports = {
|
|
131
134
|
parseExpirationToUtcDate,
|
|
132
135
|
classifyAssociationExpiration,
|
|
@@ -134,6 +137,7 @@ module.exports = {
|
|
|
134
137
|
summarizeExpirationOutcomes,
|
|
135
138
|
memberNeedsAssociationExpirationBackfill,
|
|
136
139
|
getTodayInAssociationTimeZone,
|
|
140
|
+
isAssociationExpirationCurrent,
|
|
137
141
|
ASSOCIATION_EXPIRATION_FIELD,
|
|
138
142
|
ASSOCIATION_TIME_ZONE,
|
|
139
143
|
EXPIRATION_OUTCOMES,
|
|
@@ -4,6 +4,10 @@ const { COLLECTIONS, MEMBERS_FIELDS } = require('../public/consts.js');
|
|
|
4
4
|
const { findMainAddress } = require('../public/Utils/sharedUtils.js');
|
|
5
5
|
const { calculateDistance, shuffleArray } = require('../public/Utils/sharedUtils.js');
|
|
6
6
|
|
|
7
|
+
const {
|
|
8
|
+
getTodayInAssociationTimeZone,
|
|
9
|
+
ASSOCIATION_EXPIRATION_FIELD,
|
|
10
|
+
} = require('./association-expiry');
|
|
7
11
|
const {
|
|
8
12
|
GEO_HASH_PRECISION,
|
|
9
13
|
MAX__MEMBERS_SEARCH_RESULTS,
|
|
@@ -34,6 +38,11 @@ function buildMembersSearchQuery(data) {
|
|
|
34
38
|
.ne('action', 'drop')
|
|
35
39
|
.ne('memberships.membertype', MEMBERSHIPS_TYPES.PAC_STAFF)
|
|
36
40
|
.eq('isVisible', true);
|
|
41
|
+
|
|
42
|
+
// Filter in the query, not after it: count() and skip() in run() then page over the
|
|
43
|
+
// filtered set. Do not publish this to a site whose backfill has not run - members with no
|
|
44
|
+
// date are excluded, and before the backfill that is all of them.
|
|
45
|
+
query = query.ge(ASSOCIATION_EXPIRATION_FIELD, getTodayInAssociationTimeZone());
|
|
37
46
|
let filterConfig = [
|
|
38
47
|
{
|
|
39
48
|
filterKey: 'practiceAreas',
|
package/backend/consts.js
CHANGED
|
@@ -43,6 +43,12 @@ const LOGIN_EMAIL_SYNC_STATUS = {
|
|
|
43
43
|
SKIPPED: 'skipped', // member has no wixMemberId, nothing to change
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
// Thrown by the data layer, recognised by the login layer, which turns it into the ordinary error
|
|
47
|
+
// response rather than a 500.
|
|
48
|
+
const LOGIN_REFUSAL_REASONS = {
|
|
49
|
+
ASSOCIATION_MEMBERSHIP_EXPIRED: 'ASSOCIATION_MEMBERSHIP_EXPIRED',
|
|
50
|
+
};
|
|
51
|
+
|
|
46
52
|
module.exports = {
|
|
47
53
|
CONFIG_KEYS,
|
|
48
54
|
MAX__MEMBERS_SEARCH_RESULTS,
|
|
@@ -55,4 +61,5 @@ module.exports = {
|
|
|
55
61
|
SSO_TOKEN_AUTH_API_URL,
|
|
56
62
|
BACKUP_API_URL,
|
|
57
63
|
LOGIN_EMAIL_SYNC_STATUS,
|
|
64
|
+
LOGIN_REFUSAL_REASONS,
|
|
58
65
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { ADDRESS_STATUS_TYPES } = require('../../public/consts');
|
|
2
|
+
const { ASSOCIATION_EXPIRATION_FIELD } = require('../association-expiry');
|
|
2
3
|
const { findMemberById, getMemberBySlug } = require('../members-data-methods');
|
|
3
4
|
const { isValidArray, generateGeoHash } = require('../utils');
|
|
4
5
|
|
|
@@ -198,6 +199,9 @@ async function createCoreMemberData(inputMemberData, existingDbMember, currentPa
|
|
|
198
199
|
memberships: inputMemberData.memberships,
|
|
199
200
|
pageNumber: currentPageNumber,
|
|
200
201
|
isVisible: inputMemberData.action !== MEMBER_ACTIONS.DROP,
|
|
202
|
+
// Belongs here, not in getNewMemberOnlyFields: that returns {} for existing members, so the
|
|
203
|
+
// date would never refresh on renewal and a member who paid would stay hidden. null = hidden.
|
|
204
|
+
[ASSOCIATION_EXPIRATION_FIELD]: inputMemberData[ASSOCIATION_EXPIRATION_FIELD] ?? null,
|
|
201
205
|
|
|
202
206
|
// Handle Member emails
|
|
203
207
|
...getMemberEmails(),
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { taskManager } = require('psdev-task-manager');
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
resolveAssociationExpiration,
|
|
5
|
+
ASSOCIATION_EXPIRATION_FIELD,
|
|
6
|
+
} = require('../association-expiry');
|
|
3
7
|
const { CONFIG_KEYS } = require('../consts');
|
|
4
8
|
const { fetchPACMembers } = require('../pac-api-methods');
|
|
5
9
|
const { TASKS_NAMES } = require('../tasks/consts');
|
|
@@ -126,9 +130,12 @@ async function synchronizeSinglePage(taskObject) {
|
|
|
126
130
|
}
|
|
127
131
|
return isUpdatedMember(member);
|
|
128
132
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
// Narrow each member to this site's association: licenses filtered to it, and its expiration
|
|
134
|
+
// lifted out of the memberships array into a scalar the directory query can filter on.
|
|
135
|
+
const toSyncMembersWithFilteredLicenses = toSyncMembers.map(member => ({
|
|
136
|
+
...filterLicensesByAssociation(member, siteAssociation),
|
|
137
|
+
[ASSOCIATION_EXPIRATION_FIELD]: resolveAssociationExpiration(member, siteAssociation),
|
|
138
|
+
}));
|
|
132
139
|
if (toSyncMembers.length === 0) {
|
|
133
140
|
return {
|
|
134
141
|
success: true,
|
package/backend/jobs.js
CHANGED
|
@@ -134,7 +134,7 @@ async function runDailyPullExecutionCheck(options = {}) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
* One-off backfill of associationExpiration. Run with `{ dryRun: true }` first
|
|
137
|
+
* One-off backfill of associationExpiration. Run with `{ dryRun: true }` first: it counts the
|
|
138
138
|
* members that resolve to no date, and would therefore be hidden, without writing anything.
|
|
139
139
|
* @param {Object} [options]
|
|
140
140
|
* @param {boolean} [options.dryRun]
|
|
@@ -3,7 +3,8 @@ const { createHmac } = require('crypto');
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const { decode } = require('jwt-js-decode');
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { isAssociationExpirationCurrent } = require('../association-expiry');
|
|
7
|
+
const { CONFIG_KEYS, SSO_TOKEN_AUTH_API_URL, LOGIN_REFUSAL_REASONS } = require('../consts');
|
|
7
8
|
const { MEMBER_ACTIONS } = require('../daily-pull/consts');
|
|
8
9
|
const { getCurrentMember } = require('../members-area-methods');
|
|
9
10
|
const { getCMSMemberByWixMemberId, prepareMemberForSSOLogin } = require('../members-data-methods');
|
|
@@ -79,6 +80,15 @@ async function validateMemberToken(memberIdInput) {
|
|
|
79
80
|
return invalidTokenResponse;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// Ends an already-open session once the association lapses. Without this, a member logged in
|
|
84
|
+
// before their expiry date keeps editing a listing the directory no longer shows.
|
|
85
|
+
if (!isAssociationExpirationCurrent(memberData)) {
|
|
86
|
+
console.log(
|
|
87
|
+
`[validateMemberToken] association membership expired for memberId ${memberData.memberId}`
|
|
88
|
+
);
|
|
89
|
+
return invalidTokenResponse;
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
// Add computed properties
|
|
83
93
|
memberData.addressDisplayOption = getAddressDisplayOptions(memberData);
|
|
84
94
|
console.log('memberData', memberData);
|
|
@@ -134,7 +144,14 @@ const authenticateSSOToken = async ({ token }) => {
|
|
|
134
144
|
if (isValidToken) {
|
|
135
145
|
const jwt = decode(responseToken);
|
|
136
146
|
const payload = jwt.payload;
|
|
137
|
-
|
|
147
|
+
let memberData;
|
|
148
|
+
try {
|
|
149
|
+
memberData = await prepareMemberForSSOLogin(payload);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error.message !== LOGIN_REFUSAL_REASONS.ASSOCIATION_MEMBERSHIP_EXPIRED) throw error;
|
|
152
|
+
console.log('[authenticateSSOToken] refusing login, association membership expired');
|
|
153
|
+
return { type: 'error', memberId: '', sessionToken: '' };
|
|
154
|
+
}
|
|
138
155
|
console.log('memberDataCollectionId', memberData._id);
|
|
139
156
|
const sessionToken = await generateMemberSessionToken(memberData.email);
|
|
140
157
|
const authObj = {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const { COLLECTIONS } = require('../public/consts');
|
|
2
2
|
const { isWixHostedImage, emailsMatch, normalizeEmail } = require('../public/Utils/sharedUtils');
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { isAssociationExpirationCurrent } = require('./association-expiry');
|
|
5
|
+
const { MEMBERSHIPS_TYPES, LOGIN_REFUSAL_REASONS } = require('./consts');
|
|
5
6
|
const { createSiteContact } = require('./contacts-methods');
|
|
6
7
|
const { MEMBER_ACTIONS } = require('./daily-pull/consts');
|
|
7
8
|
const { wixData } = require('./elevated-modules');
|
|
@@ -561,8 +562,8 @@ const getAllMembersWithoutContactFormEmail = async () => {
|
|
|
561
562
|
};
|
|
562
563
|
|
|
563
564
|
/**
|
|
564
|
-
* Every member, unfiltered.
|
|
565
|
-
*
|
|
565
|
+
* Every member, unfiltered. Filtering would hide the very records the expiry backfill's report
|
|
566
|
+
* exists to surface - the ones with no readable expiration.
|
|
566
567
|
* @returns {Promise<Array>} - Array of member data
|
|
567
568
|
*/
|
|
568
569
|
const getAllMembers = async () => {
|
|
@@ -696,6 +697,14 @@ async function prepareMemberForSSOLogin(data) {
|
|
|
696
697
|
if (!memberData) {
|
|
697
698
|
throw new Error(`Member data not found for memberId ${memberId}`);
|
|
698
699
|
}
|
|
700
|
+
// Before ensureWixMemberAndContactExist, which creates the Wix member and contact when they are
|
|
701
|
+
// missing - refusing later would still provision an account for someone who cannot use it.
|
|
702
|
+
if (!isAssociationExpirationCurrent(memberData)) {
|
|
703
|
+
console.log(
|
|
704
|
+
`[prepareMemberForSSOLogin] refusing login, association membership expired for memberId ${memberId}`
|
|
705
|
+
);
|
|
706
|
+
throw new Error(LOGIN_REFUSAL_REASONS.ASSOCIATION_MEMBERSHIP_EXPIRED);
|
|
707
|
+
}
|
|
699
708
|
console.log('memberData', memberData);
|
|
700
709
|
return await ensureWixMemberAndContactExist(memberData);
|
|
701
710
|
} catch (error) {
|
package/backend/routers/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { getMainAddress } = require('../../public/Utils/sharedUtils');
|
|
2
|
+
const { isAssociationExpirationCurrent } = require('../association-expiry');
|
|
2
3
|
const { getMemberBySlug } = require('../members-data-methods');
|
|
3
4
|
const {
|
|
4
5
|
getMoreAddressesToDisplay,
|
|
@@ -113,6 +114,13 @@ const getMemberProfileData = async (slug, siteAssociation) => {
|
|
|
113
114
|
return null;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Gated here rather than in getMemberBySlug: that also backs URL uniqueness during the sync,
|
|
118
|
+
// where an expired member's slug must still count as taken or a new member could claim it.
|
|
119
|
+
if (!isAssociationExpirationCurrent(member)) {
|
|
120
|
+
console.log(`[getMemberProfileData] Association membership expired for slug: ${slug}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
return transformMemberToProfileData(member, siteAssociation);
|
|
117
125
|
} catch (error) {
|
|
118
126
|
const errorMessage = `Error in getMemberProfileData for slug: ${slug} : ${error.message}`;
|
|
@@ -256,7 +256,9 @@ const TASKS = {
|
|
|
256
256
|
getIdentifier: task => task.data,
|
|
257
257
|
process: associationExpiryBackfillChunk,
|
|
258
258
|
shouldSkipCheck: () => false,
|
|
259
|
-
|
|
259
|
+
// A packing budget, not a timeout: the manager fills each 240s tick with tasks costing
|
|
260
|
+
// estimate x 1.5. Chunks measured at ~6.5s, so 10 gives 16 per tick with room to spare.
|
|
261
|
+
estimatedDurationSec: 10,
|
|
260
262
|
},
|
|
261
263
|
[TASKS_NAMES.dailyPullExecutionCheck]: {
|
|
262
264
|
name: TASKS_NAMES.dailyPullExecutionCheck,
|