abmp-npm 10.3.21 → 10.3.24

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.
@@ -2,6 +2,7 @@
2
2
  // cannot drift on what "expired" means. Deliberately free of Wix imports.
3
3
 
4
4
  const ASSOCIATION_EXPIRATION_FIELD = 'associationExpiration';
5
+ const PAC_STAFF_MEMBERTYPE = 'PAC STAFF';
5
6
  const ASSOCIATION_TIME_ZONE = 'America/Denver';
6
7
 
7
8
  const EXPIRATION_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})/;
@@ -128,7 +129,20 @@ const isAssociationExpirationCurrent = (member, now) => {
128
129
  return expiration.getTime() >= getTodayInAssociationTimeZone(now).getTime();
129
130
  };
130
131
 
132
+ // PAC staff accounts carry a rolling "expiration" equal to the date of the feed that last mentioned
133
+ // them, so the association gate would lock a staff member out on any day the feed did not resend
134
+ // them (ticket 13031107600). Staff are already excluded from the directory and have no public
135
+ // profile; login is the one place the gate reaches them, and it should not.
136
+ const isPacStaff = member =>
137
+ Array.isArray(member?.memberships) &&
138
+ member.memberships.some(membership => membership?.membertype === PAC_STAFF_MEMBERTYPE);
139
+
140
+ const isLoginAllowedByExpiration = (member, now) =>
141
+ isPacStaff(member) || isAssociationExpirationCurrent(member, now);
142
+
131
143
  module.exports = {
144
+ isPacStaff,
145
+ isLoginAllowedByExpiration,
132
146
  parseExpirationToUtcDate,
133
147
  classifyAssociationExpiration,
134
148
  resolveAssociationExpiration,
@@ -244,9 +244,15 @@ async function fetchAllItemsInParallel(query) {
244
244
  const batchSize = WIX_QUERY_MAX_LIMIT;
245
245
  const allItems = [];
246
246
 
247
- const firstResult = await query.skip(0).limit(batchSize).find();
247
+ // @wix/data leaves totalPages undefined unless the count is requested explicitly, so reading it
248
+ // off the first page silently stopped this at 1,000 rows. Count first; count() is already what
249
+ // the typed search relies on.
250
+ const [totalCount, firstResult] = await Promise.all([
251
+ query.count(),
252
+ query.skip(0).limit(batchSize).find(),
253
+ ]);
248
254
 
249
- const totalBatches = firstResult.totalPages;
255
+ const totalBatches = Math.ceil(totalCount / batchSize);
250
256
  allItems.push(...firstResult.items);
251
257
 
252
258
  if (totalBatches > 1) {
@@ -3,7 +3,7 @@ const { createHmac } = require('crypto');
3
3
  const axios = require('axios');
4
4
  const { decode } = require('jwt-js-decode');
5
5
 
6
- const { isAssociationExpirationCurrent } = require('../association-expiry');
6
+ const { isLoginAllowedByExpiration } = require('../association-expiry');
7
7
  const { CONFIG_KEYS, SSO_TOKEN_AUTH_API_URL, LOGIN_REFUSAL_REASONS } = require('../consts');
8
8
  const { MEMBER_ACTIONS } = require('../daily-pull/consts');
9
9
  const { getCurrentMember } = require('../members-area-methods');
@@ -80,7 +80,7 @@ async function validateMemberToken(memberIdInput) {
80
80
  return invalidTokenResponse;
81
81
  }
82
82
 
83
- if (!isAssociationExpirationCurrent(memberData)) {
83
+ if (!isLoginAllowedByExpiration(memberData)) {
84
84
  console.log(
85
85
  `[validateMemberToken] association membership expired for memberId ${memberData.memberId}`
86
86
  );
@@ -1,7 +1,7 @@
1
1
  const { COLLECTIONS } = require('../public/consts');
2
2
  const { isWixHostedImage, emailsMatch, normalizeEmail } = require('../public/Utils/sharedUtils');
3
3
 
4
- const { isAssociationExpirationCurrent } = require('./association-expiry');
4
+ const { isLoginAllowedByExpiration } = require('./association-expiry');
5
5
  const { MEMBERSHIPS_TYPES, LOGIN_REFUSAL_REASONS } = require('./consts');
6
6
  const { createSiteContact } = require('./contacts-methods');
7
7
  const { MEMBER_ACTIONS } = require('./daily-pull/consts');
@@ -701,7 +701,7 @@ async function prepareMemberForSSOLogin(data) {
701
701
  if (!memberData) {
702
702
  throw new Error(`Member data not found for memberId ${memberId}`);
703
703
  }
704
- if (!isAssociationExpirationCurrent(memberData)) {
704
+ if (!isLoginAllowedByExpiration(memberData)) {
705
705
  console.log(
706
706
  `[prepareMemberForSSOLogin] refusing login, association membership expired for memberId ${memberId}`
707
707
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "10.3.21",
3
+ "version": "10.3.24",
4
4
  "main": "index.js",
5
5
  "files": [
6
6
  "index.js",
package/pages/Profile.js CHANGED
@@ -2,6 +2,7 @@ const { location: wixLocation } = require('@wix/site-location');
2
2
  const { window: wixWindow } = require('@wix/site-window');
3
3
 
4
4
  const { LIGHTBOX_NAMES } = require('../public/consts');
5
+ const { hasCopyrightYear, withCurrentCopyrightYear } = require('../public/Utils/copyrightYear');
5
6
  const {
6
7
  generateId,
7
8
  formatPracticeAreasForDisplay,
@@ -20,6 +21,26 @@ const BREAKPOINTS = {
20
21
  TABLET: 750,
21
22
  };
22
23
 
24
+ // The footer reads "© <year> <name>. All rights reserved.": the name goes into #fullNameTextFoter,
25
+ // but the year was typed into each site's editor and has no nickname of its own, so ABMP stayed on
26
+ // 2025 while ASCP and AHP said 2026 (PAC ticket 12914830766). The three profile pages declare the
27
+ // same plain text elements; the copyright notice is found among them by content and its year kept
28
+ // current, so no editor needs touching now or next January.
29
+ const PLAIN_TEXT_ELEMENT_IDS = [
30
+ '#text389',
31
+ '#text178',
32
+ '#text150',
33
+ '#text155',
34
+ '#text157',
35
+ '#text158',
36
+ '#text164',
37
+ '#text169',
38
+ '#text171',
39
+ '#text175',
40
+ '#text177',
41
+ '#text179',
42
+ ];
43
+
23
44
  async function profileOnReady({ $w: _$w }) {
24
45
  const profileData = await wixWindow.getRouterData();
25
46
  console.log('profileData', profileData);
@@ -97,6 +118,23 @@ async function profileOnReady({ $w: _$w }) {
97
118
  bindStudentBadge();
98
119
  bindProfileImages();
99
120
  bindFullName();
121
+ bindCopyrightYear();
122
+ }
123
+
124
+ function bindCopyrightYear() {
125
+ PLAIN_TEXT_ELEMENT_IDS.forEach(id => {
126
+ try {
127
+ const element = _$w(id);
128
+ // .html keeps the element's formatting; fall back to .text for elements without markup.
129
+ if (hasCopyrightYear(element.html)) {
130
+ element.html = withCurrentCopyrightYear(element.html);
131
+ } else if (hasCopyrightYear(element.text)) {
132
+ element.text = withCurrentCopyrightYear(element.text);
133
+ }
134
+ } catch {
135
+ // The element does not exist on this site's page: nothing to update.
136
+ }
137
+ });
100
138
  }
101
139
 
102
140
  function bindMemberSince() {
@@ -0,0 +1,35 @@
1
+ // The member website footer reads "© <year> <member name>. All rights reserved." The year was typed
2
+ // into each site's editor, so ABMP stayed on 2025 while ASCP and AHP said 2026 (PAC ticket
3
+ // 12914830766). This keeps the year current without anyone touching the editors again.
4
+ // Deliberately free of Wix imports so it can be unit-tested.
5
+
6
+ const ASSOCIATION_TIME_ZONE = 'America/Denver';
7
+ // Matches the year in "© 2025", "&copy; 2025" (the .html of a text element), "(c) 2025" and
8
+ // "Copyright 2025", and nothing else — "Member since 2025" is left alone.
9
+ const COPYRIGHT_YEAR_PATTERN = /(©|&copy;|\(c\)|copyright)(\s*|&nbsp;)(\d{4})/i;
10
+
11
+ // PAC operates in Denver; the year should roll over at their midnight, not the visitor's.
12
+ const getCurrentYear = (now = new Date()) => {
13
+ try {
14
+ return Number(
15
+ new Intl.DateTimeFormat('en-US', { timeZone: ASSOCIATION_TIME_ZONE, year: 'numeric' }).format(
16
+ now
17
+ )
18
+ );
19
+ } catch {
20
+ return now.getUTCFullYear();
21
+ }
22
+ };
23
+
24
+ /** Returns the text with the year after the copyright mark set to the current year, or unchanged. */
25
+ const withCurrentCopyrightYear = (text, now = new Date()) => {
26
+ if (typeof text !== 'string') return text;
27
+ return text.replace(
28
+ COPYRIGHT_YEAR_PATTERN,
29
+ (_, mark, space) => `${mark}${space}${getCurrentYear(now)}`
30
+ );
31
+ };
32
+
33
+ const hasCopyrightYear = text => typeof text === 'string' && COPYRIGHT_YEAR_PATTERN.test(text);
34
+
35
+ module.exports = { getCurrentYear, withCurrentCopyrightYear, hasCopyrightYear };