@7365admin1/core 3.64.3-staging.285 → 3.64.3-staging.287

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,31 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Let a signed-in member hold their own HID QR code.
6
+
7
+ `GET`, `POST` and `DELETE /readers/:readerId/profile-qr` read, issue and revoke
8
+ the caller's own QR credential on one reader. The caller comes from the session,
9
+ so there is no user id in the path to point at somebody else, and each handler
10
+ calls `authorizeReader` for itself rather than trusting the router - the site is
11
+ re-resolved from the stored reader record.
12
+
13
+ The credential is the visitor mechanism: an alphanumeric value in the reader's
14
+ `qrcodes` table, with the reader put into the QR-readable configuration first.
15
+ Unlike a visitor pass it carries no validity window, matching the member's
16
+ facial enrollment, which is standing until removed. Issuing replaces any earlier
17
+ code for that member on that reader, and the replacement is written and verified
18
+ before the old one is removed.
19
+
20
+ It creates no access rule, deliberately. `setProfileFacial` beside it does not
21
+ either: a member's door access comes from the HID permissions screen, and
22
+ minting a rule here would mean anyone who can press Generate admits themselves.
23
+ The read reports whether that screen admits them, resolved through
24
+ `resolvePermissionUserBindings` - the same resolver that writes the rules onto
25
+ the device - so the answer is the one the reader will actually be asked.
26
+
27
+ Two settings are now readable that were already writable: the QR identification
28
+ and legacy-mode flags `configureVisitorQrReader` sets on every issue, plus
29
+ `access_rule_time_zones` alongside its two sibling link tables. Whether a reader
30
+ was set up to accept a QR, and whether an access rule had a schedule, were
31
+ invisible to every screen and every diagnosis.
@@ -0,0 +1,22 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Make the search on the HID reader user roster work.
6
+
7
+ `listReaderUsers` asked the device to filter. Amico's `where` takes a value or a
8
+ list of values per column and has no operator form, so the substring clause it
9
+ sent for a text term, `{name: {LIKE: "%x%"}}`, was answered
10
+ `400 Invalid value (string or array expected)`. Every search by name threw, and
11
+ the screen showed "Unable to load HID users from reader" with an empty table.
12
+
13
+ A numeric term took a different branch that matched the HID user id exactly.
14
+ That did not fail, it answered wrongly: searching a registration number like
15
+ `000004` resolved to `id = 4` and returned a different person.
16
+
17
+ Rows are already pulled in one page and filtered here for `userType`, so the
18
+ search now joins that filter as a case-insensitive substring over name, id and
19
+ registration - the same three fields, and the same "contains" semantics, the
20
+ identities search uses. Verified against a live reader: a name matches
21
+ partially and case-insensitively, a registration number returns its own user,
22
+ a full UID still works, and no match returns an empty page rather than an error.
@@ -0,0 +1,16 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Stop refusing Site Settings saves that send back `metadata.residentAppModulesEffective`.
6
+
7
+ `GET /api/sites/:id` attaches the derived `residentAppModulesEffective` to
8
+ `metadata` on every read, and the Site Settings screens save by sending the
9
+ metadata they read straight back. `updateSiteSchema` does not allow that key, so
10
+ `PATCH /api/sites/id/:id` refused the whole save with "is not allowed" — the
11
+ resident-app configuration panel, the HID QR code pass and the HID face-reader
12
+ toggle all failed.
13
+
14
+ `updateById` now drops the derived key before validating. It is never stored and
15
+ is recomputed on the next read, so nothing is lost; every stored key, including
16
+ `residentAppModules`, is validated and saved exactly as before.
package/dist/index.d.ts CHANGED
@@ -12642,6 +12642,48 @@ declare function useHidAmicoService(): {
12642
12642
  facialEnrolled: boolean;
12643
12643
  facialEnrolledAt: string;
12644
12644
  }>;
12645
+ getProfileQr: (readerId: string, userId: string) => Promise<{
12646
+ readerId: string;
12647
+ userId: string;
12648
+ memberId: string;
12649
+ hidUserId: string;
12650
+ registration: string;
12651
+ readerName: string;
12652
+ portalName: string;
12653
+ qrEnabled: boolean;
12654
+ issued: boolean;
12655
+ qrValue: string;
12656
+ issuedAt: string;
12657
+ assigned: boolean | null;
12658
+ }>;
12659
+ issueProfileQr: (readerId: string, userId: string) => Promise<{
12660
+ readerId: string;
12661
+ userId: string;
12662
+ memberId: string;
12663
+ hidUserId: string;
12664
+ registration: string;
12665
+ readerName: string;
12666
+ portalName: string;
12667
+ qrEnabled: boolean;
12668
+ issued: boolean;
12669
+ qrValue: string;
12670
+ issuedAt: string;
12671
+ assigned: boolean | null;
12672
+ }>;
12673
+ deleteProfileQr: (readerId: string, userId: string) => Promise<{
12674
+ readerId: string;
12675
+ userId: string;
12676
+ memberId: string;
12677
+ hidUserId: string;
12678
+ registration: string;
12679
+ readerName: string;
12680
+ portalName: string;
12681
+ qrEnabled: boolean;
12682
+ issued: boolean;
12683
+ qrValue: string;
12684
+ issuedAt: string;
12685
+ assigned: boolean | null;
12686
+ }>;
12645
12687
  setProfileFacial: (readerId: string, userId: string, image: Buffer, options?: {
12646
12688
  timestamp?: number;
12647
12689
  match?: boolean;
@@ -12891,6 +12933,9 @@ declare function useHidAmicoController(): {
12891
12933
  revokeVisitorQr: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12892
12934
  getProfileFacial: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12893
12935
  setProfileFacial: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12936
+ getProfileQr: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12937
+ issueProfileQr: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12938
+ deleteProfileQr: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12894
12939
  setVisitorImage: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12895
12940
  deleteVisitorImage: (req: Request, res: Response, next: NextFunction) => Promise<void>;
12896
12941
  getIntercomStatus: (req: Request, res: Response, next: NextFunction) => Promise<void>;
package/dist/index.js CHANGED
@@ -38517,6 +38517,13 @@ function residentAppModulesAboveCeiling(orgModules, requested) {
38517
38517
  function residentAppModulesAboveCeilingMessage(rejected) {
38518
38518
  return `This client has not been given ${rejected.join(", ")} in the resident app, so a site cannot switch it on. Ask Seven365 to add it to the organisation first.`;
38519
38519
  }
38520
+ function withoutDerivedSiteMetadata(metadata) {
38521
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
38522
+ return metadata;
38523
+ }
38524
+ const { residentAppModulesEffective: _derived, ...stored } = metadata;
38525
+ return stored;
38526
+ }
38520
38527
  function asModules(value) {
38521
38528
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
38522
38529
  }
@@ -38704,8 +38711,9 @@ function useSiteController() {
38704
38711
  }
38705
38712
  async function updateById(req, res, next) {
38706
38713
  try {
38714
+ const body = req.body?.metadata ? { ...req.body, metadata: withoutDerivedSiteMetadata(req.body.metadata) } : req.body;
38707
38715
  const { error, value } = updateSiteSchema.validate(
38708
- { _id: req.params.id, ...req.body },
38716
+ { _id: req.params.id, ...body },
38709
38717
  { abortEarly: false }
38710
38718
  );
38711
38719
  if (error) {
@@ -48716,7 +48724,12 @@ var HID_READABLE_OBJECTS = /* @__PURE__ */ new Set([
48716
48724
  "time_spans",
48717
48725
  "group_access_rules",
48718
48726
  "portal_access_rules",
48719
- "user_access_rules"
48727
+ "user_access_rules",
48728
+ // The third leg of the authorization chain, alongside the two above. A rule
48729
+ // with no time zone never grants, so leaving this unreadable made the most
48730
+ // common misconfiguration the one nobody could see. Read-only: the mutable
48731
+ // set is unchanged.
48732
+ "access_rule_time_zones"
48720
48733
  ]);
48721
48734
  var HID_WRITABLE_CONFIGURATION = /* @__PURE__ */ new Map([
48722
48735
  ["identifier", /* @__PURE__ */ new Set([
@@ -48727,10 +48740,18 @@ var HID_WRITABLE_CONFIGURATION = /* @__PURE__ */ new Map([
48727
48740
  ["pjsip", new Set(PJSIP_CONFIGURATION_KEYS)]
48728
48741
  ]);
48729
48742
  var HID_READABLE_CONFIGURATION = /* @__PURE__ */ new Map([
48730
- ["general", /* @__PURE__ */ new Set(["online"])],
48743
+ // `local_identification` and the two QR settings below are written by
48744
+ // `configureVisitorQrReader` on every visitor-QR issue but could not be read
48745
+ // back, so whether a reader is actually set up to accept a QR was invisible
48746
+ // to every screen and every diagnosis. Reading a setting we already write is
48747
+ // not a widening of what this endpoint can change - the writable map is
48748
+ // untouched.
48749
+ ["general", /* @__PURE__ */ new Set(["online", "local_identification"])],
48750
+ ["face_id", /* @__PURE__ */ new Set(["qrcode_legacy_mode_enabled"])],
48731
48751
  ["identifier", /* @__PURE__ */ new Set([
48732
48752
  "card_identification_enabled",
48733
48753
  "pin_identification_enabled",
48754
+ "qrcode_identification_enabled",
48734
48755
  "multi_factor_authentication"
48735
48756
  ])],
48736
48757
  // A configured password must never be returned to a browser client.
@@ -51116,20 +51137,21 @@ function useHidAmicoService() {
51116
51137
  const client = new HidAmicoClient(reader);
51117
51138
  try {
51118
51139
  await client.login();
51119
- const normalizedSearch = search.trim();
51120
- const numericSearch = /^\d{1,15}$/.test(normalizedSearch) ? Number(normalizedSearch) : null;
51121
- const userWhere = {};
51122
- if (numericSearch !== null && Number.isSafeInteger(numericSearch)) {
51123
- userWhere.id = numericSearch;
51124
- } else if (normalizedSearch) {
51125
- userWhere.name = { LIKE: `%${normalizedSearch}%` };
51126
- }
51140
+ const normalizedSearch = search.trim().toLowerCase();
51141
+ const matchesSearch = (row) => {
51142
+ if (!normalizedSearch)
51143
+ return true;
51144
+ return [row.name, row.id, row.registration].some((value) => String(value ?? "").toLowerCase().includes(normalizedSearch));
51145
+ };
51127
51146
  let rows = [];
51128
51147
  let reportedTotal = null;
51129
51148
  let hasMore = false;
51130
51149
  const response = await client.loadObjects({
51131
51150
  object: "users",
51132
- where: { users: userWhere },
51151
+ where: { users: {} },
51152
+ // One page of the device's table, then everything below is decided
51153
+ // here. A reader holding more than this many users would need paging
51154
+ // added; the estate's readers hold single figures.
51133
51155
  limit: 500,
51134
51156
  offset: 0,
51135
51157
  order: ["id", "ascending"]
@@ -51137,7 +51159,8 @@ function useHidAmicoService() {
51137
51159
  const deviceRows = getHidObjectRows(response, "users");
51138
51160
  const filteredRows = deviceRows.filter((row) => {
51139
51161
  const isVisitor = Number(row.user_type_id) === 1;
51140
- return userType === "visitor" ? isVisitor : userType === "user" ? !isVisitor : true;
51162
+ const typeMatches = userType === "visitor" ? isVisitor : userType === "user" ? !isVisitor : true;
51163
+ return typeMatches && matchesSearch(row);
51141
51164
  });
51142
51165
  reportedTotal = filteredRows.length;
51143
51166
  rows = filteredRows.slice(offset, offset + limit);
@@ -51821,6 +51844,198 @@ function useHidAmicoService() {
51821
51844
  }
51822
51845
  return null;
51823
51846
  }
51847
+ function createProfileQrCredential(readerId, userId, qrValue) {
51848
+ return {
51849
+ credentialId: createStableHidId(`profile-qr:${readerId}:${userId}`),
51850
+ object: "qrcodes",
51851
+ value: qrValue || `USR${import_crypto3.default.randomBytes(12).toString("hex").toUpperCase()}`
51852
+ };
51853
+ }
51854
+ async function isProfileAssignedToReader(reader, userId) {
51855
+ try {
51856
+ const documents = await repo.listSitePermissionDocuments(String(reader.site));
51857
+ const assignments = documents.flatMap((document2) => Array.isArray(document2.assignments) ? document2.assignments : []);
51858
+ if (!assignments.length)
51859
+ return false;
51860
+ const bindings = await repo.resolvePermissionUserBindings(String(reader.site), assignments);
51861
+ return bindings.some((binding) => String(binding.userId) === String(userId));
51862
+ } catch {
51863
+ return null;
51864
+ }
51865
+ }
51866
+ async function getProfileQr(readerId, userId) {
51867
+ const reader = await getActiveReader(readerId);
51868
+ const context = await getProfileEnrollmentContext(readerId, reader, userId);
51869
+ const metadata = context.profileIdentity && isUnknownRecord2(context.profileIdentity.metadata) ? context.profileIdentity.metadata : {};
51870
+ return {
51871
+ readerId,
51872
+ userId,
51873
+ memberId: String(context.member._id),
51874
+ hidUserId: String(context.hidUserId),
51875
+ registration: context.registration,
51876
+ readerName: String(reader.name || ""),
51877
+ portalName: String(reader.portalName || ""),
51878
+ qrEnabled: readerSupportsCredential(reader, "qr"),
51879
+ issued: typeof metadata.profileQrValue === "string" && metadata.profileQrValue.length > 0,
51880
+ qrValue: typeof metadata.profileQrValue === "string" ? metadata.profileQrValue : "",
51881
+ issuedAt: typeof metadata.profileQrIssuedAt === "string" ? metadata.profileQrIssuedAt : "",
51882
+ assigned: await isProfileAssignedToReader(reader, userId)
51883
+ };
51884
+ }
51885
+ async function issueProfileQr(readerId, userId) {
51886
+ const reader = await getActiveReader(readerId);
51887
+ assertReaderCapability(reader, "qr");
51888
+ assertReaderPortal(reader, "issuing a QR code");
51889
+ const context = await getProfileEnrollmentContext(readerId, reader, userId);
51890
+ if (!context.profileName) {
51891
+ throw new import_node_server_utils137.BadRequestError("A profile name is required to issue a HID QR code.");
51892
+ }
51893
+ const issuedAt = /* @__PURE__ */ new Date();
51894
+ const credential = createProfileQrCredential(readerId, userId);
51895
+ const client = new HidAmicoClient(reader);
51896
+ try {
51897
+ await client.login();
51898
+ await configureVisitorQrReader(client);
51899
+ const existingUserResponse = await client.loadObjects({
51900
+ object: "users",
51901
+ where: { users: { id: context.hidUserId } },
51902
+ limit: 1,
51903
+ offset: 0
51904
+ });
51905
+ const hidUser = { name: context.profileName, registration: context.registration };
51906
+ if (getHidObjectRows(existingUserResponse, "users").length) {
51907
+ await client.modifyObjects({
51908
+ object: "users",
51909
+ where: { users: { id: context.hidUserId } },
51910
+ values: hidUser
51911
+ });
51912
+ } else {
51913
+ await client.createObjects({
51914
+ object: "users",
51915
+ values: [{ id: context.hidUserId, ...hidUser }]
51916
+ });
51917
+ }
51918
+ const existingQrs = getHidObjectRows(
51919
+ await client.loadObjects({
51920
+ object: credential.object,
51921
+ where: { [credential.object]: { user_id: context.hidUserId } },
51922
+ limit: 100,
51923
+ offset: 0
51924
+ }),
51925
+ credential.object
51926
+ );
51927
+ const credentialValues = { value: credential.value, user_id: context.hidUserId };
51928
+ if (existingQrs.some((row) => String(row.id) === String(credential.credentialId))) {
51929
+ await client.modifyObjects({
51930
+ object: credential.object,
51931
+ where: { [credential.object]: { id: credential.credentialId } },
51932
+ values: credentialValues
51933
+ });
51934
+ } else {
51935
+ await client.createObjects({
51936
+ object: credential.object,
51937
+ values: [{ id: credential.credentialId, ...credentialValues }]
51938
+ });
51939
+ }
51940
+ const savedCredential = getHidObjectRows(
51941
+ await client.loadObjects({
51942
+ object: credential.object,
51943
+ where: { [credential.object]: { id: credential.credentialId } },
51944
+ limit: 1,
51945
+ offset: 0
51946
+ }),
51947
+ credential.object
51948
+ )[0];
51949
+ if (!savedCredential || String(savedCredential.user_id) !== String(context.hidUserId) || String(savedCredential.value) !== String(credential.value)) {
51950
+ throw new import_node_server_utils137.BadRequestError("The HID reader did not retain the generated QR code.");
51951
+ }
51952
+ for (const staleQr of existingQrs) {
51953
+ const staleId = Number(staleQr.id);
51954
+ if (Number.isSafeInteger(staleId) && staleId > 0 && staleId !== credential.credentialId) {
51955
+ await client.destroyObjects({
51956
+ object: credential.object,
51957
+ where: { [credential.object]: { id: staleId } }
51958
+ });
51959
+ }
51960
+ }
51961
+ const existingIdentity = context.profileIdentity;
51962
+ const existingMetadata = existingIdentity && isUnknownRecord2(existingIdentity.metadata) ? existingIdentity.metadata : {};
51963
+ const identityPayload = {
51964
+ hidUserId: String(context.hidUserId),
51965
+ registration: context.registration,
51966
+ user: userId,
51967
+ member: context.member._id,
51968
+ type: context.identityType,
51969
+ status: "active",
51970
+ metadata: {
51971
+ ...existingMetadata,
51972
+ name: context.profileName,
51973
+ profileQr: true,
51974
+ profileQrValue: credential.value,
51975
+ profileQrCredentialId: credential.credentialId,
51976
+ profileQrIssuedAt: issuedAt.toISOString()
51977
+ }
51978
+ };
51979
+ if (existingIdentity?._id) {
51980
+ await repo.updateIdentity(existingIdentity._id, identityPayload);
51981
+ } else {
51982
+ await repo.addIdentity({
51983
+ ...identityPayload,
51984
+ reader: readerId,
51985
+ site: reader.site
51986
+ });
51987
+ }
51988
+ await repo.updateById(readerId, { lastSeenAt: /* @__PURE__ */ new Date() });
51989
+ await repo.addEvent({
51990
+ reader: readerId,
51991
+ site: reader.site,
51992
+ type: "profile_qr_issued",
51993
+ payload: {
51994
+ user: userId,
51995
+ member: String(context.member._id),
51996
+ hidUserId: context.hidUserId,
51997
+ credentialId: credential.credentialId
51998
+ }
51999
+ });
52000
+ return getProfileQr(readerId, userId);
52001
+ } catch (error) {
52002
+ await repo.addEvent({
52003
+ reader: readerId,
52004
+ site: reader.site,
52005
+ type: "profile_qr_issue_failed",
52006
+ payload: { user: userId, message: getErrorMessage2(error) }
52007
+ });
52008
+ throw error;
52009
+ } finally {
52010
+ await client.logout();
52011
+ }
52012
+ }
52013
+ async function deleteProfileQr(readerId, userId) {
52014
+ const reader = await getActiveReader(readerId);
52015
+ const context = await getProfileEnrollmentContext(readerId, reader, userId);
52016
+ const client = new HidAmicoClient(reader);
52017
+ try {
52018
+ await client.login();
52019
+ await client.destroyObjects({
52020
+ object: "qrcodes",
52021
+ where: { qrcodes: { user_id: context.hidUserId } }
52022
+ });
52023
+ if (context.profileIdentity?._id) {
52024
+ const existingMetadata = isUnknownRecord2(context.profileIdentity.metadata) ? context.profileIdentity.metadata : {};
52025
+ const { profileQr, profileQrValue, profileQrCredentialId, profileQrIssuedAt, ...rest } = existingMetadata;
52026
+ await repo.updateIdentity(context.profileIdentity._id, { metadata: rest });
52027
+ }
52028
+ await repo.addEvent({
52029
+ reader: readerId,
52030
+ site: reader.site,
52031
+ type: "profile_qr_revoked",
52032
+ payload: { user: userId, hidUserId: context.hidUserId }
52033
+ });
52034
+ return getProfileQr(readerId, userId);
52035
+ } finally {
52036
+ await client.logout();
52037
+ }
52038
+ }
51824
52039
  async function getProfileFacial(readerId, userId) {
51825
52040
  const reader = await getActiveReader(readerId);
51826
52041
  const context = await getProfileEnrollmentContext(readerId, reader, userId);
@@ -53366,6 +53581,9 @@ function useHidAmicoService() {
53366
53581
  queueVisitorQrJob,
53367
53582
  processVisitorQrGatewayJobs,
53368
53583
  getProfileFacial,
53584
+ getProfileQr,
53585
+ issueProfileQr,
53586
+ deleteProfileQr,
53369
53587
  setProfileFacial,
53370
53588
  setVisitorImage,
53371
53589
  deleteVisitorImage,
@@ -93472,6 +93690,45 @@ function useHidAmicoController() {
93472
93690
  next(error);
93473
93691
  }
93474
93692
  }
93693
+ async function getProfileQr(req, res, next) {
93694
+ const { error, value } = schemaHidAmicoReaderIdParams.validate(req.params);
93695
+ if (error) {
93696
+ next(new import_node_server_utils301.BadRequestError(error.message));
93697
+ return;
93698
+ }
93699
+ try {
93700
+ await authorizeReader(req, value.readerId);
93701
+ res.json({ data: await service.getProfileQr(value.readerId, getAuthenticatedUserId(req)) });
93702
+ } catch (error2) {
93703
+ next(error2);
93704
+ }
93705
+ }
93706
+ async function issueProfileQr(req, res, next) {
93707
+ const { error, value } = schemaHidAmicoReaderIdParams.validate(req.params);
93708
+ if (error) {
93709
+ next(new import_node_server_utils301.BadRequestError(error.message));
93710
+ return;
93711
+ }
93712
+ try {
93713
+ await authorizeReader(req, value.readerId);
93714
+ res.json({ data: await service.issueProfileQr(value.readerId, getAuthenticatedUserId(req)) });
93715
+ } catch (error2) {
93716
+ next(error2);
93717
+ }
93718
+ }
93719
+ async function deleteProfileQr(req, res, next) {
93720
+ const { error, value } = schemaHidAmicoReaderIdParams.validate(req.params);
93721
+ if (error) {
93722
+ next(new import_node_server_utils301.BadRequestError(error.message));
93723
+ return;
93724
+ }
93725
+ try {
93726
+ await authorizeReader(req, value.readerId);
93727
+ res.json({ data: await service.deleteProfileQr(value.readerId, getAuthenticatedUserId(req)) });
93728
+ } catch (error2) {
93729
+ next(error2);
93730
+ }
93731
+ }
93475
93732
  async function setProfileFacial(req, res, next) {
93476
93733
  const params = schemaHidAmicoReaderIdParams.validate(req.params);
93477
93734
  const query2 = schemaHidAmicoUserImageUploadQuery.validate(req.query);
@@ -93698,6 +93955,9 @@ function useHidAmicoController() {
93698
93955
  revokeVisitorQr,
93699
93956
  getProfileFacial,
93700
93957
  setProfileFacial,
93958
+ getProfileQr,
93959
+ issueProfileQr,
93960
+ deleteProfileQr,
93701
93961
  setVisitorImage,
93702
93962
  deleteVisitorImage,
93703
93963
  getIntercomStatus,