@7365admin1/core 3.64.5-staging.307 → 3.64.5-staging.310

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.
@@ -0,0 +1,5 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Re-inviting the same person to the same app, organisation and site now re-sends the pending invitation instead of adding a duplicate; invite request logs no longer record the invitee's e-mail address.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,220 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.68.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 1d5050b: Re-inviting the same person to the same app, organisation and site now re-sends the pending invitation instead of adding a duplicate; invite request logs no longer record the invitee's e-mail address.
8
+
9
+ ## 3.68.2
10
+
11
+ ### Patch Changes
12
+
13
+ - d9e0ded: A duplicate membership insert now returns 400 "The user is already a member of this organization." instead of a 500.
14
+
15
+ ## 3.68.1
16
+
17
+ ### Patch Changes
18
+
19
+ - 689b152: POST /api/people: count cross-client person creation (gate ships switched off)
20
+
21
+ The signed-in branch of `POST /api/people` checked only that somebody was signed
22
+ in. The site and organisation came straight from the request body, and where that
23
+ body carries an e-mail, a password and a resident/tenant type, the service goes on
24
+ to create a **user account** and an **active membership** as well as the person row.
25
+
26
+ `site.people` is the collection every site-reach decision on the platform reads its
27
+ verdict from, so a row written there is not an ordinary record — it manufactures
28
+ reach into another client's estate.
29
+
30
+ This adds the site-reach check the camera, HID and people-list paths already use,
31
+ behind `PEOPLE_CREATE_REACH_MODE`, and **ships it in `log` mode**: it records what
32
+ it would refuse and refuses nothing. `enforce` refuses; `off` does not look. Any
33
+ unexpected error allows the request — this must never be why a real registration
34
+ fails.
35
+
36
+ It ships off because of RP-5. A person row carries no `createdBy`, so who created
37
+ the existing rows cannot be established retrospectively and the true refusal count
38
+ is only knowable from live traffic. Measured read-only on staging: 372
39
+ `site.people` rows, of which exactly **1** is self-signup shaped (`platform:
40
+ "mobile"`, `status: "pending"`) and 371 were written by a signed-in caller, across
41
+ 3 of 256 sites.
42
+
43
+ Pre-login self-signup is untouched in every mode: a request with no session takes
44
+ the other branch of `add` before this runs, and that route is already constrained
45
+ by its own allow-list schema with `status` and `platform` pinned server-side.
46
+
47
+ ## 3.68.0
48
+
49
+ ### Minor Changes
50
+
51
+ - d7e9298: Manpower live status: an authenticated namespace beside the open one
52
+
53
+ `/manpower` streams every client site's live staffing level — how many guards are
54
+ actually on shift against how many were promised — to anyone who connects, with no
55
+ credential of any kind.
56
+
57
+ This adds `/manpower-live`, carrying exactly the same `manpower-status-update`
58
+ events, which admits a socket only when the handshake presents a session id that
59
+ resolves to a live session. Every refusal is an identical `Unauthorized`, and a
60
+ session store that is down refuses rather than letting everyone in.
61
+
62
+ `/manpower` itself is deliberately unchanged: the Capacitor guard app installed on
63
+ phones today and the Nuxt security screen both connect to it with no credential and
64
+ would be cut off the moment it were tightened. Every update is emitted to both, so
65
+ the two feeds can never disagree, and both namespaces share one connection handler,
66
+ so a client on the authenticated feed still starts the poll and still gets an
67
+ immediate snapshot.
68
+
69
+ A live session is the whole check, deliberately. Scoping the feed to the caller's
70
+ own sites is not possible today: manpower lives in HRM Labs' vendor id space, none
71
+ of those site ids match ours, and no field links the two — a site check would empty
72
+ the screen for the one customer using it. That mapping is a schema change and
73
+ belongs to the leads.
74
+
75
+ ### Patch Changes
76
+
77
+ - 87c8679: Module gate: the kill switch now stops the 409, and the check stops reading when there is nothing to decide
78
+
79
+ Two follow-ups from the module-gate server review, both behaviour-neutral for
80
+ anybody who is not using the kill switch.
81
+
82
+ **`MODULE_LIST_GATE=off` now means off.** Every other part of the gate checked the
83
+ master switch; `mustAcknowledge` did not. So with the gate switched off, a save that
84
+ narrowed a client's module list was still refused with a 409 "acknowledge the impact
85
+ first" — the one lever a lead would reach for in an emergency, not working for the
86
+ one path that blocks a save from going through. Both the organisation and the site
87
+ save paths go through that function, so both are fixed. With the switch at its
88
+ default the 409 behaves exactly as before, and that is asserted.
89
+
90
+ **`moduleDeniedForCaller` no longer reads memberships when no list is enforced.** It
91
+ runs on every governed request and read the caller's memberships before finding out
92
+ whether there was anything to decide — and `members.user` has no index to use. It now
93
+ returns early unless the site or its organisation has an enforced list, which is what
94
+ the notification filter already did. No verdict changes: a client that really has an
95
+ enforced list is still measured exactly as before, and that is asserted too.
96
+
97
+ Also corrects the 3.65.0 changelog, which claimed `MODULE_LIST_GATE` switched all of
98
+ the gate off at once. It did not, for the reason above.
99
+
100
+ ## 3.67.0
101
+
102
+ ### Minor Changes
103
+
104
+ - 7b9ea2f: Notifications: stop sending alerts for modules a client was never given
105
+
106
+ `NOTIFY_ACCESS_FILTER` now defaults to `on` instead of `log`, so the module-based
107
+ drop is real: a recipient no longer gets a notification when every membership they
108
+ hold at the site denies every module the notification's category is about.
109
+
110
+ Only the module drop acts. The role-based check is still computed and logged and
111
+ never drops anybody, because most apps do not enforce their role gates yet and a
112
+ role-based drop would hide alerts for screens people can still open.
113
+
114
+ Unchanged: residents, anyone with no membership at the site, Seven365 staff, sends
115
+ with no site, and categories the catalogue does not govern are all exempt; any
116
+ error sends to everyone; and `MODULE_LIST_GATE=off` still disables it entirely.
117
+ `NOTIFY_ACCESS_FILTER=log` or `off` overrides this default with no deploy.
118
+
119
+ This is inert until a client's module list is enforced — measured read-only on
120
+ staging at the time of the flip, no organisation (0 of 199) and no site (0 of 256)
121
+ had a stored list and there were no acknowledged-save markers, so nothing is
122
+ dropped for anybody until a Super Admin saves a module list with the acknowledged
123
+ preview.
124
+
125
+ ## 3.66.0
126
+
127
+ ### Minor Changes
128
+
129
+ - b7f6302: Add a staff-only handler that repairs an organisation's missing owner membership.
130
+
131
+ An organisation created before 3.60.0 can exist with no `members` row at all, so
132
+ `GET /api/members/user/:id/app/organization` answers 404 and every console
133
+ landing page tells the real owner that no organisation exists. The new
134
+ `orgOwnerRepairHandlers` writes the one membership the onboarding path already
135
+ writes, for one organisation at a time, and only when that organisation is
136
+ active, holds exactly zero `organization` memberships, and has an account
137
+ registered under its own e-mail address. Staff-only (`organizations` console
138
+ permission), idempotent under a one-writer claim, audited, and behind the
139
+ `ORG_OWNER_REPAIR` switch. No schema change and no migration.
140
+
141
+ ## 3.65.2
142
+
143
+ ### Patch Changes
144
+
145
+ - fcd2f1e: `GET /api/members/users/:id` no longer shows anybody's memberships to any signed-in account. The person and Seven365 staff still get the full list; anyone else gets only the memberships in organisations they belong to, were invited to or own (an empty list, not an error, when there are none).
146
+
147
+ ## 3.65.1
148
+
149
+ ### Patch Changes
150
+
151
+ - 69714b4: Role writes: a site's own module list narrows a role only when the site belongs to the role's organisation. A service provider's role at a client's site is no longer refused modules the client switched off for that site — matching what the role editor now offers.
152
+
153
+ ## 3.65.0
154
+
155
+ ### Minor Changes
156
+
157
+ - c4dc50b: Module gate, server step: the deny-list endpoint, the impact preview, and
158
+ acknowledge-before-save. Everything ships inert and fails open.
159
+
160
+ - `useMemberController().getModulesByUserIdType` (for
161
+ `GET /api/members/user/:id/app/:type/modules?org=&site=`) answers
162
+ `{ denied: string[] }` for the membership `getByUserIdType` answers with. It
163
+ is the same self-or-staff guard. It answers `[]` for no list, an empty list,
164
+ a resident, Seven365 staff, a site of another organisation, any lookup
165
+ error, and `MODULE_LIST_GATE=off`. `ALWAYS_ALLOWED` is never denied.
166
+ - A saved list is enforced only once it has been saved through the
167
+ acknowledged preview. The marker is the save's own append-only console-audit
168
+ row (`gateListHash`). A list saved earlier, or changed any other way, behaves
169
+ as absent. No field is added to any organisation or site.
170
+ - `useOrgController().previewOrgModules` and
171
+ `useSiteController().previewSiteModules` (for
172
+ `POST /api/organizations/:id/modules/preview` and
173
+ `POST /api/sites/:id/modules/preview`) return who would lose what. They are
174
+ read-only and never go through `GET /api/roles`.
175
+ - `updateModules` and `updateSiteModules` accept `acknowledgeImpact`. A save
176
+ that takes a module away from somebody is refused with 409 until it is
177
+ acknowledged. An empty list restores everything and never needs it. Site
178
+ saves are now recorded in the console audit (`site.modules-changed`).
179
+ - `filterByAccess` (Q7) runs at the top of the notification `send()`. It is
180
+ `NOTIFY_ACCESS_FILTER=log` by default: it logs who would be filtered and
181
+ still sends to everyone. It is exported so API-core's own sender can call it.
182
+ - `moduleDeniedForCaller` (Q3) is the server-refusal primitive. It is
183
+ `MODULE_GATE_MODE=log` by default and never refuses. `enforce` refuses only
184
+ the keys in `MODULE_GATE_ENFORCE_KEYS`. Work stamped (`at`) before the
185
+ acknowledged save is accepted.
186
+ - `MODULE_LIST_GATE` (code default `on`) switches all of the above off at
187
+ once. CORRECTION (3.68.0): it did not. `mustAcknowledge` never read it, so
188
+ with the gate off a narrowing save was still refused with a 409. Fixed in
189
+ 3.68.0; every other path did check it.
190
+
191
+ ### Patch Changes
192
+
193
+ - c993ee4: POST /api/members/direct refuses (400) a role that belongs to a different organisation, unless the caller already holds that role in the organisation being joined.
194
+ - 74f228f: POST /api/members/direct: adding yourself now needs a role you may give yourself - Seven365 staff, the organisation's owner, a role you already hold there, or the role your own invitation offers. Enrolling somebody else is unchanged.
195
+ - ae42c1c: GET /api/organizations/id/:id returns the whole organisation record only to a caller with a relationship to it (member, invitee, owner, Seven365 staff, an active resident of one of its sites, or an engaged service provider). Everyone else gets the name-only record the anonymous lookup already returns. It never answers 401/403.
196
+ - 7363b8d: Site Settings saves are no longer refused because the client has one resident-app
197
+ switch turned off. A save is refused only when it would turn ON a resident-app
198
+ switch that the site has stored as off and the client has switched off.
199
+ - 9d1a0f0: PATCH /api/users/field/:id (and v2): on your own account you may change only name, contact, nric, dateOfBirth, profile and gender, and defaultOrg to an organisation you reach. status, email and plateNumber are staff-only.
200
+
201
+ ## 3.64.5
202
+
203
+ ### Patch Changes
204
+
205
+ - 49e1aef: A person's role now follows the site they are on.
206
+
207
+ `GET /api/members/user/:id/app/:type` accepts an optional `?site=` (a 24-hex
208
+ site id, like `?org=`). When it is sent, the answer is the person's live
209
+ membership of that app type pinned to that site (inside `?org=` when it is also
210
+ sent); failing that, their organisation-wide membership in `?org=`; failing
211
+ that, any live membership in `?org=`. When none of these exists the route
212
+ answers exactly as before. A removed membership never wins, and a site never
213
+ turns a found membership into "not found". Callers that do not send `site`
214
+ see no change. Login, auth-v2 and verification are unchanged.
215
+
216
+ The rule is the pure `pickSiteMembership` in `utils/member-lookup.util.ts`.
217
+
3
218
  ## 3.64.4
4
219
 
5
220
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1057,6 +1057,10 @@ declare class MVerification implements TVerification {
1057
1057
  constructor(value: TVerification);
1058
1058
  }
1059
1059
 
1060
+ type TKeyValuePair<K extends string | number | symbol = string, V = any> = {
1061
+ [key in K]: V;
1062
+ };
1063
+
1060
1064
  declare function useVerificationRepo(): {
1061
1065
  createIndex: () => Promise<void>;
1062
1066
  createTextIndex: () => Promise<void>;
@@ -1076,6 +1080,10 @@ declare function useVerificationRepo(): {
1076
1080
  }) => Promise<{}>;
1077
1081
  getByIdByType: (type: string) => Promise<TVerification[]>;
1078
1082
  updateStatusById: (_id: string | ObjectId, status: string, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
1083
+ reissuePendingById: (_id: string | ObjectId, { metadata, expireAt }: {
1084
+ metadata: TKeyValuePair;
1085
+ expireAt: string;
1086
+ }) => Promise<number>;
1079
1087
  getByStatus: (status: string) => Promise<TVerification[]>;
1080
1088
  findOne: (query: any) => Promise<mongodb.WithId<bson.Document> | null>;
1081
1089
  completePendingInvites: ({ email, orgId, siteId, app, session, }: {
@@ -1087,10 +1095,6 @@ declare function useVerificationRepo(): {
1087
1095
  }) => Promise<mongodb.UpdateResult<bson.Document>>;
1088
1096
  };
1089
1097
 
1090
- type TKeyValuePair<K extends string | number | symbol = string, V = any> = {
1091
- [key in K]: V;
1092
- };
1093
-
1094
1098
  declare function useVerificationService(): {
1095
1099
  createUserInvite: ({ email, metadata, }: {
1096
1100
  email: string;
package/dist/index.js CHANGED
@@ -11278,9 +11278,6 @@ var import_node_server_utils45 = require("@7365admin1/node-server-utils");
11278
11278
  // src/services/verification.service.ts
11279
11279
  var import_node_server_utils41 = require("@7365admin1/node-server-utils");
11280
11280
 
11281
- // src/repositories/verification.repo.ts
11282
- var import_node_server_utils19 = require("@7365admin1/node-server-utils");
11283
-
11284
11281
  // src/models/verification.model.ts
11285
11282
  var import_node_server_utils18 = require("@7365admin1/node-server-utils");
11286
11283
  var import_mongodb16 = require("mongodb");
@@ -11329,6 +11326,7 @@ var MVerification = class {
11329
11326
  };
11330
11327
 
11331
11328
  // src/repositories/verification.repo.ts
11329
+ var import_node_server_utils19 = require("@7365admin1/node-server-utils");
11332
11330
  var import_mongodb17 = require("mongodb");
11333
11331
  function useVerificationRepo() {
11334
11332
  const db2 = import_node_server_utils19.useAtlas.getDb();
@@ -11558,6 +11556,28 @@ function useVerificationRepo() {
11558
11556
  throw new import_node_server_utils19.InternalServerError("Error updating verification status.");
11559
11557
  }
11560
11558
  }
11559
+ async function reissuePendingById(_id, { metadata, expireAt }) {
11560
+ try {
11561
+ _id = new import_mongodb17.ObjectId(_id);
11562
+ } catch (error) {
11563
+ throw new import_node_server_utils19.BadRequestError("Invalid verification ID format.");
11564
+ }
11565
+ try {
11566
+ const result = await collection.updateOne(
11567
+ { _id, status: "pending" },
11568
+ { $set: { metadata, expireAt, updatedAt: (/* @__PURE__ */ new Date()).toISOString() } }
11569
+ );
11570
+ delNamespace().catch(
11571
+ (err) => import_node_server_utils19.logger.error(`Failed to clear cache for namespace: ${namespace_collection3}`, err)
11572
+ );
11573
+ delCache((0, import_node_server_utils19.makeCacheKey)(namespace_collection3, { _id })).catch(
11574
+ (err) => import_node_server_utils19.logger.error(`Failed to delete cache for verification ${_id}`, err)
11575
+ );
11576
+ return result.modifiedCount;
11577
+ } catch (error) {
11578
+ throw new import_node_server_utils19.InternalServerError("Error re-sending the invitation.");
11579
+ }
11580
+ }
11561
11581
  async function getByStatus(status) {
11562
11582
  try {
11563
11583
  const data = await collection.find({ status }).toArray();
@@ -11627,6 +11647,7 @@ function useVerificationRepo() {
11627
11647
  getVerifications,
11628
11648
  getByIdByType,
11629
11649
  updateStatusById,
11650
+ reissuePendingById,
11630
11651
  getByStatus,
11631
11652
  findOne,
11632
11653
  completePendingInvites
@@ -17616,7 +17637,8 @@ function useVerificationService() {
17616
17637
  add,
17617
17638
  getById: _getById,
17618
17639
  updateStatusById: _updateStatusById,
17619
- getByStatus: _getByStatus
17640
+ getByStatus: _getByStatus,
17641
+ reissuePendingById
17620
17642
  } = useVerificationRepo();
17621
17643
  const { getUserByEmail } = useUserRepo();
17622
17644
  const { getById: getOrgById, getByEmail: getOrgByEmail } = useOrgRepo();
@@ -17691,13 +17713,6 @@ function useVerificationService() {
17691
17713
  }
17692
17714
  const verificationIds = [];
17693
17715
  for (const app of apps) {
17694
- await useVerificationRepo().findOne({
17695
- type,
17696
- email,
17697
- "metadata.app": app,
17698
- "metadata.org": metadata.org,
17699
- "metadata.siteId": metadata.siteId
17700
- });
17701
17716
  const value = {
17702
17717
  type,
17703
17718
  email,
@@ -17708,7 +17723,24 @@ function useVerificationService() {
17708
17723
  expireAt: new Date(Date.now() + 72 * 60 * 60 * 1e3).toISOString(),
17709
17724
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
17710
17725
  };
17711
- const createdId = await add(value);
17726
+ const stored = new MVerification({
17727
+ ...value,
17728
+ metadata: { ...value.metadata }
17729
+ }).metadata;
17730
+ const pending = await useVerificationRepo().findOne({
17731
+ type,
17732
+ // same address in any letter case, as `completePendingInvites` reads it
17733
+ email: new RegExp(`^${email.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&")}$`, "i"),
17734
+ status: "pending",
17735
+ "metadata.app": app,
17736
+ "metadata.org": stored.org,
17737
+ "metadata.siteId": stored.siteId
17738
+ });
17739
+ const reissued = pending?._id && await reissuePendingById(pending._id, {
17740
+ metadata: stored,
17741
+ expireAt: value.expireAt
17742
+ });
17743
+ const createdId = reissued ? pending._id : await add(value);
17712
17744
  verificationIds.push(createdId.toString());
17713
17745
  const link = `${APP_MAIN}/verify/invitation/${createdId}`;
17714
17746
  const emailContent = (0, import_node_server_utils41.compileHandlebar)({
@@ -29973,6 +30005,7 @@ function useMemberController() {
29973
30005
  // src/controllers/verification.controller.ts
29974
30006
  var import_node_server_utils66 = require("@7365admin1/node-server-utils");
29975
30007
  var import_joi28 = __toESM(require("joi"));
30008
+ var withoutEmail = (payload) => JSON.stringify({ ...payload, email: payload?.email ? "[redacted]" : void 0 });
29976
30009
  function callerId2(req) {
29977
30010
  const user = req.user;
29978
30011
  return user?.user?.toString?.() || user?._id?.toString?.();
@@ -30039,7 +30072,7 @@ function useVerificationController() {
30039
30072
  const payload = { ...req.body };
30040
30073
  import_node_server_utils66.logger.log({
30041
30074
  level: "info",
30042
- message: `[invite/user] payload: ${JSON.stringify(payload)}`
30075
+ message: `[invite/user] payload: ${withoutEmail(payload)}`
30043
30076
  });
30044
30077
  const validation = import_joi28.default.object({
30045
30078
  email: import_joi28.default.string().email().required(),
@@ -30095,7 +30128,7 @@ function useVerificationController() {
30095
30128
  } catch (error2) {
30096
30129
  import_node_server_utils66.logger.log({
30097
30130
  level: "error",
30098
- message: `[invite/user] error for payload ${JSON.stringify(payload)}: ${error2.message}`
30131
+ message: `[invite/user] error for payload ${withoutEmail(payload)}: ${error2.message}`
30099
30132
  });
30100
30133
  next(error2);
30101
30134
  return;
@@ -30105,7 +30138,7 @@ function useVerificationController() {
30105
30138
  const payload = { ...req.body };
30106
30139
  import_node_server_utils66.logger.log({
30107
30140
  level: "info",
30108
- message: `[invite/member] payload: ${JSON.stringify(payload)}`
30141
+ message: `[invite/member] payload: ${withoutEmail(payload)}`
30109
30142
  });
30110
30143
  const validation = import_joi28.default.object({
30111
30144
  email: import_joi28.default.string().email().required(),
@@ -30157,7 +30190,7 @@ function useVerificationController() {
30157
30190
  } catch (error2) {
30158
30191
  import_node_server_utils66.logger.log({
30159
30192
  level: "error",
30160
- message: `[invite/member] error for payload ${JSON.stringify(payload)}: ${error2.message}`
30193
+ message: `[invite/member] error for payload ${withoutEmail(payload)}: ${error2.message}`
30161
30194
  });
30162
30195
  next(error2);
30163
30196
  return;
@@ -30167,7 +30200,7 @@ function useVerificationController() {
30167
30200
  const payload = req.body;
30168
30201
  import_node_server_utils66.logger.log({
30169
30202
  level: "info",
30170
- message: `[service-providers/invite] payload: ${JSON.stringify(payload)}`
30203
+ message: `[service-providers/invite] payload: ${withoutEmail(payload)}`
30171
30204
  });
30172
30205
  const validation = import_joi28.default.object({
30173
30206
  email: import_joi28.default.string().email().required(),
@@ -30224,7 +30257,7 @@ function useVerificationController() {
30224
30257
  } catch (error2) {
30225
30258
  import_node_server_utils66.logger.log({
30226
30259
  level: "error",
30227
- message: `[service-providers/invite] error for payload ${JSON.stringify(payload)}: ${error2.message}`
30260
+ message: `[service-providers/invite] error for payload ${withoutEmail(payload)}: ${error2.message}`
30228
30261
  });
30229
30262
  next(error2);
30230
30263
  return;