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

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.
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
@@ -48716,7 +48716,12 @@ var HID_READABLE_OBJECTS = /* @__PURE__ */ new Set([
48716
48716
  "time_spans",
48717
48717
  "group_access_rules",
48718
48718
  "portal_access_rules",
48719
- "user_access_rules"
48719
+ "user_access_rules",
48720
+ // The third leg of the authorization chain, alongside the two above. A rule
48721
+ // with no time zone never grants, so leaving this unreadable made the most
48722
+ // common misconfiguration the one nobody could see. Read-only: the mutable
48723
+ // set is unchanged.
48724
+ "access_rule_time_zones"
48720
48725
  ]);
48721
48726
  var HID_WRITABLE_CONFIGURATION = /* @__PURE__ */ new Map([
48722
48727
  ["identifier", /* @__PURE__ */ new Set([
@@ -48727,10 +48732,18 @@ var HID_WRITABLE_CONFIGURATION = /* @__PURE__ */ new Map([
48727
48732
  ["pjsip", new Set(PJSIP_CONFIGURATION_KEYS)]
48728
48733
  ]);
48729
48734
  var HID_READABLE_CONFIGURATION = /* @__PURE__ */ new Map([
48730
- ["general", /* @__PURE__ */ new Set(["online"])],
48735
+ // `local_identification` and the two QR settings below are written by
48736
+ // `configureVisitorQrReader` on every visitor-QR issue but could not be read
48737
+ // back, so whether a reader is actually set up to accept a QR was invisible
48738
+ // to every screen and every diagnosis. Reading a setting we already write is
48739
+ // not a widening of what this endpoint can change - the writable map is
48740
+ // untouched.
48741
+ ["general", /* @__PURE__ */ new Set(["online", "local_identification"])],
48742
+ ["face_id", /* @__PURE__ */ new Set(["qrcode_legacy_mode_enabled"])],
48731
48743
  ["identifier", /* @__PURE__ */ new Set([
48732
48744
  "card_identification_enabled",
48733
48745
  "pin_identification_enabled",
48746
+ "qrcode_identification_enabled",
48734
48747
  "multi_factor_authentication"
48735
48748
  ])],
48736
48749
  // A configured password must never be returned to a browser client.
@@ -51116,20 +51129,21 @@ function useHidAmicoService() {
51116
51129
  const client = new HidAmicoClient(reader);
51117
51130
  try {
51118
51131
  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
- }
51132
+ const normalizedSearch = search.trim().toLowerCase();
51133
+ const matchesSearch = (row) => {
51134
+ if (!normalizedSearch)
51135
+ return true;
51136
+ return [row.name, row.id, row.registration].some((value) => String(value ?? "").toLowerCase().includes(normalizedSearch));
51137
+ };
51127
51138
  let rows = [];
51128
51139
  let reportedTotal = null;
51129
51140
  let hasMore = false;
51130
51141
  const response = await client.loadObjects({
51131
51142
  object: "users",
51132
- where: { users: userWhere },
51143
+ where: { users: {} },
51144
+ // One page of the device's table, then everything below is decided
51145
+ // here. A reader holding more than this many users would need paging
51146
+ // added; the estate's readers hold single figures.
51133
51147
  limit: 500,
51134
51148
  offset: 0,
51135
51149
  order: ["id", "ascending"]
@@ -51137,7 +51151,8 @@ function useHidAmicoService() {
51137
51151
  const deviceRows = getHidObjectRows(response, "users");
51138
51152
  const filteredRows = deviceRows.filter((row) => {
51139
51153
  const isVisitor = Number(row.user_type_id) === 1;
51140
- return userType === "visitor" ? isVisitor : userType === "user" ? !isVisitor : true;
51154
+ const typeMatches = userType === "visitor" ? isVisitor : userType === "user" ? !isVisitor : true;
51155
+ return typeMatches && matchesSearch(row);
51141
51156
  });
51142
51157
  reportedTotal = filteredRows.length;
51143
51158
  rows = filteredRows.slice(offset, offset + limit);
@@ -51821,6 +51836,198 @@ function useHidAmicoService() {
51821
51836
  }
51822
51837
  return null;
51823
51838
  }
51839
+ function createProfileQrCredential(readerId, userId, qrValue) {
51840
+ return {
51841
+ credentialId: createStableHidId(`profile-qr:${readerId}:${userId}`),
51842
+ object: "qrcodes",
51843
+ value: qrValue || `USR${import_crypto3.default.randomBytes(12).toString("hex").toUpperCase()}`
51844
+ };
51845
+ }
51846
+ async function isProfileAssignedToReader(reader, userId) {
51847
+ try {
51848
+ const documents = await repo.listSitePermissionDocuments(String(reader.site));
51849
+ const assignments = documents.flatMap((document2) => Array.isArray(document2.assignments) ? document2.assignments : []);
51850
+ if (!assignments.length)
51851
+ return false;
51852
+ const bindings = await repo.resolvePermissionUserBindings(String(reader.site), assignments);
51853
+ return bindings.some((binding) => String(binding.userId) === String(userId));
51854
+ } catch {
51855
+ return null;
51856
+ }
51857
+ }
51858
+ async function getProfileQr(readerId, userId) {
51859
+ const reader = await getActiveReader(readerId);
51860
+ const context = await getProfileEnrollmentContext(readerId, reader, userId);
51861
+ const metadata = context.profileIdentity && isUnknownRecord2(context.profileIdentity.metadata) ? context.profileIdentity.metadata : {};
51862
+ return {
51863
+ readerId,
51864
+ userId,
51865
+ memberId: String(context.member._id),
51866
+ hidUserId: String(context.hidUserId),
51867
+ registration: context.registration,
51868
+ readerName: String(reader.name || ""),
51869
+ portalName: String(reader.portalName || ""),
51870
+ qrEnabled: readerSupportsCredential(reader, "qr"),
51871
+ issued: typeof metadata.profileQrValue === "string" && metadata.profileQrValue.length > 0,
51872
+ qrValue: typeof metadata.profileQrValue === "string" ? metadata.profileQrValue : "",
51873
+ issuedAt: typeof metadata.profileQrIssuedAt === "string" ? metadata.profileQrIssuedAt : "",
51874
+ assigned: await isProfileAssignedToReader(reader, userId)
51875
+ };
51876
+ }
51877
+ async function issueProfileQr(readerId, userId) {
51878
+ const reader = await getActiveReader(readerId);
51879
+ assertReaderCapability(reader, "qr");
51880
+ assertReaderPortal(reader, "issuing a QR code");
51881
+ const context = await getProfileEnrollmentContext(readerId, reader, userId);
51882
+ if (!context.profileName) {
51883
+ throw new import_node_server_utils137.BadRequestError("A profile name is required to issue a HID QR code.");
51884
+ }
51885
+ const issuedAt = /* @__PURE__ */ new Date();
51886
+ const credential = createProfileQrCredential(readerId, userId);
51887
+ const client = new HidAmicoClient(reader);
51888
+ try {
51889
+ await client.login();
51890
+ await configureVisitorQrReader(client);
51891
+ const existingUserResponse = await client.loadObjects({
51892
+ object: "users",
51893
+ where: { users: { id: context.hidUserId } },
51894
+ limit: 1,
51895
+ offset: 0
51896
+ });
51897
+ const hidUser = { name: context.profileName, registration: context.registration };
51898
+ if (getHidObjectRows(existingUserResponse, "users").length) {
51899
+ await client.modifyObjects({
51900
+ object: "users",
51901
+ where: { users: { id: context.hidUserId } },
51902
+ values: hidUser
51903
+ });
51904
+ } else {
51905
+ await client.createObjects({
51906
+ object: "users",
51907
+ values: [{ id: context.hidUserId, ...hidUser }]
51908
+ });
51909
+ }
51910
+ const existingQrs = getHidObjectRows(
51911
+ await client.loadObjects({
51912
+ object: credential.object,
51913
+ where: { [credential.object]: { user_id: context.hidUserId } },
51914
+ limit: 100,
51915
+ offset: 0
51916
+ }),
51917
+ credential.object
51918
+ );
51919
+ const credentialValues = { value: credential.value, user_id: context.hidUserId };
51920
+ if (existingQrs.some((row) => String(row.id) === String(credential.credentialId))) {
51921
+ await client.modifyObjects({
51922
+ object: credential.object,
51923
+ where: { [credential.object]: { id: credential.credentialId } },
51924
+ values: credentialValues
51925
+ });
51926
+ } else {
51927
+ await client.createObjects({
51928
+ object: credential.object,
51929
+ values: [{ id: credential.credentialId, ...credentialValues }]
51930
+ });
51931
+ }
51932
+ const savedCredential = getHidObjectRows(
51933
+ await client.loadObjects({
51934
+ object: credential.object,
51935
+ where: { [credential.object]: { id: credential.credentialId } },
51936
+ limit: 1,
51937
+ offset: 0
51938
+ }),
51939
+ credential.object
51940
+ )[0];
51941
+ if (!savedCredential || String(savedCredential.user_id) !== String(context.hidUserId) || String(savedCredential.value) !== String(credential.value)) {
51942
+ throw new import_node_server_utils137.BadRequestError("The HID reader did not retain the generated QR code.");
51943
+ }
51944
+ for (const staleQr of existingQrs) {
51945
+ const staleId = Number(staleQr.id);
51946
+ if (Number.isSafeInteger(staleId) && staleId > 0 && staleId !== credential.credentialId) {
51947
+ await client.destroyObjects({
51948
+ object: credential.object,
51949
+ where: { [credential.object]: { id: staleId } }
51950
+ });
51951
+ }
51952
+ }
51953
+ const existingIdentity = context.profileIdentity;
51954
+ const existingMetadata = existingIdentity && isUnknownRecord2(existingIdentity.metadata) ? existingIdentity.metadata : {};
51955
+ const identityPayload = {
51956
+ hidUserId: String(context.hidUserId),
51957
+ registration: context.registration,
51958
+ user: userId,
51959
+ member: context.member._id,
51960
+ type: context.identityType,
51961
+ status: "active",
51962
+ metadata: {
51963
+ ...existingMetadata,
51964
+ name: context.profileName,
51965
+ profileQr: true,
51966
+ profileQrValue: credential.value,
51967
+ profileQrCredentialId: credential.credentialId,
51968
+ profileQrIssuedAt: issuedAt.toISOString()
51969
+ }
51970
+ };
51971
+ if (existingIdentity?._id) {
51972
+ await repo.updateIdentity(existingIdentity._id, identityPayload);
51973
+ } else {
51974
+ await repo.addIdentity({
51975
+ ...identityPayload,
51976
+ reader: readerId,
51977
+ site: reader.site
51978
+ });
51979
+ }
51980
+ await repo.updateById(readerId, { lastSeenAt: /* @__PURE__ */ new Date() });
51981
+ await repo.addEvent({
51982
+ reader: readerId,
51983
+ site: reader.site,
51984
+ type: "profile_qr_issued",
51985
+ payload: {
51986
+ user: userId,
51987
+ member: String(context.member._id),
51988
+ hidUserId: context.hidUserId,
51989
+ credentialId: credential.credentialId
51990
+ }
51991
+ });
51992
+ return getProfileQr(readerId, userId);
51993
+ } catch (error) {
51994
+ await repo.addEvent({
51995
+ reader: readerId,
51996
+ site: reader.site,
51997
+ type: "profile_qr_issue_failed",
51998
+ payload: { user: userId, message: getErrorMessage2(error) }
51999
+ });
52000
+ throw error;
52001
+ } finally {
52002
+ await client.logout();
52003
+ }
52004
+ }
52005
+ async function deleteProfileQr(readerId, userId) {
52006
+ const reader = await getActiveReader(readerId);
52007
+ const context = await getProfileEnrollmentContext(readerId, reader, userId);
52008
+ const client = new HidAmicoClient(reader);
52009
+ try {
52010
+ await client.login();
52011
+ await client.destroyObjects({
52012
+ object: "qrcodes",
52013
+ where: { qrcodes: { user_id: context.hidUserId } }
52014
+ });
52015
+ if (context.profileIdentity?._id) {
52016
+ const existingMetadata = isUnknownRecord2(context.profileIdentity.metadata) ? context.profileIdentity.metadata : {};
52017
+ const { profileQr, profileQrValue, profileQrCredentialId, profileQrIssuedAt, ...rest } = existingMetadata;
52018
+ await repo.updateIdentity(context.profileIdentity._id, { metadata: rest });
52019
+ }
52020
+ await repo.addEvent({
52021
+ reader: readerId,
52022
+ site: reader.site,
52023
+ type: "profile_qr_revoked",
52024
+ payload: { user: userId, hidUserId: context.hidUserId }
52025
+ });
52026
+ return getProfileQr(readerId, userId);
52027
+ } finally {
52028
+ await client.logout();
52029
+ }
52030
+ }
51824
52031
  async function getProfileFacial(readerId, userId) {
51825
52032
  const reader = await getActiveReader(readerId);
51826
52033
  const context = await getProfileEnrollmentContext(readerId, reader, userId);
@@ -53366,6 +53573,9 @@ function useHidAmicoService() {
53366
53573
  queueVisitorQrJob,
53367
53574
  processVisitorQrGatewayJobs,
53368
53575
  getProfileFacial,
53576
+ getProfileQr,
53577
+ issueProfileQr,
53578
+ deleteProfileQr,
53369
53579
  setProfileFacial,
53370
53580
  setVisitorImage,
53371
53581
  deleteVisitorImage,
@@ -93472,6 +93682,45 @@ function useHidAmicoController() {
93472
93682
  next(error);
93473
93683
  }
93474
93684
  }
93685
+ async function getProfileQr(req, res, next) {
93686
+ const { error, value } = schemaHidAmicoReaderIdParams.validate(req.params);
93687
+ if (error) {
93688
+ next(new import_node_server_utils301.BadRequestError(error.message));
93689
+ return;
93690
+ }
93691
+ try {
93692
+ await authorizeReader(req, value.readerId);
93693
+ res.json({ data: await service.getProfileQr(value.readerId, getAuthenticatedUserId(req)) });
93694
+ } catch (error2) {
93695
+ next(error2);
93696
+ }
93697
+ }
93698
+ async function issueProfileQr(req, res, next) {
93699
+ const { error, value } = schemaHidAmicoReaderIdParams.validate(req.params);
93700
+ if (error) {
93701
+ next(new import_node_server_utils301.BadRequestError(error.message));
93702
+ return;
93703
+ }
93704
+ try {
93705
+ await authorizeReader(req, value.readerId);
93706
+ res.json({ data: await service.issueProfileQr(value.readerId, getAuthenticatedUserId(req)) });
93707
+ } catch (error2) {
93708
+ next(error2);
93709
+ }
93710
+ }
93711
+ async function deleteProfileQr(req, res, next) {
93712
+ const { error, value } = schemaHidAmicoReaderIdParams.validate(req.params);
93713
+ if (error) {
93714
+ next(new import_node_server_utils301.BadRequestError(error.message));
93715
+ return;
93716
+ }
93717
+ try {
93718
+ await authorizeReader(req, value.readerId);
93719
+ res.json({ data: await service.deleteProfileQr(value.readerId, getAuthenticatedUserId(req)) });
93720
+ } catch (error2) {
93721
+ next(error2);
93722
+ }
93723
+ }
93475
93724
  async function setProfileFacial(req, res, next) {
93476
93725
  const params = schemaHidAmicoReaderIdParams.validate(req.params);
93477
93726
  const query2 = schemaHidAmicoUserImageUploadQuery.validate(req.query);
@@ -93698,6 +93947,9 @@ function useHidAmicoController() {
93698
93947
  revokeVisitorQr,
93699
93948
  getProfileFacial,
93700
93949
  setProfileFacial,
93950
+ getProfileQr,
93951
+ issueProfileQr,
93952
+ deleteProfileQr,
93701
93953
  setVisitorImage,
93702
93954
  deleteVisitorImage,
93703
93955
  getIntercomStatus,