@7365admin1/core 3.52.2-staging.235 → 3.52.2-staging.236

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,11 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Vehicle management: make deleting and editing a vehicle safe against the ANPR cameras.
6
+
7
+ Deleting a vehicle no longer reports success when the plate is still on a camera. `bypass` now defaults to false, `removePlateNumber` returns an explicit per-camera outcome instead of swallowing its error, and a delete that cannot be confirmed on every camera holding the plate is refused with the record left intact so it can be retried. A site with no ANPR camera has nothing to revoke and now deletes successfully instead of failing with "No site cameras found."
8
+
9
+ Each camera is asked for its own record id before the removal, because a Dahua `recNo` is a per-device sequence number and the one stored on the vehicle only matches whichever camera was written last. Editing only the vehicle's name now removes the old whitelist record before inserting the new one, instead of leaving the previous entry on the camera for good.
10
+
11
+ Also: `pullVehicleByRecNo` is scoped to the person or site the plate belongs to (it matched on the record id alone, which is not unique across devices), the paginated camera lookups no longer skip their last page, and editing or deleting a vehicle now requires `vehicle-mgmt:update-vehicle` / `vehicle-mgmt:delete-vehicle` on the server, matching the gate the apps already apply in the browser.
package/dist/index.d.ts CHANGED
@@ -2505,6 +2505,31 @@ declare function useBuildingUnitController(): {
2505
2505
  getAllLevelsWithUnits: (req: Request, res: Response, next: NextFunction) => Promise<void>;
2506
2506
  };
2507
2507
 
2508
+ /**
2509
+ * "Was this plate actually taken off the camera?" — the one place that answers
2510
+ * it, for the three paths that revoke an ANPR whitelist/blocklist record:
2511
+ * deleting a vehicle, and the two branches of editing one.
2512
+ *
2513
+ * The reason this file exists: `removePlateNumber` used to `catch`, log, and
2514
+ * return `undefined`, so a device that refused, timed out or was unreachable
2515
+ * came back looking exactly like one that had removed the record. The delete
2516
+ * path then reported "Vehicle deleted successfully." while the plate was still
2517
+ * on the barrier — the vehicle could keep driving in after the record was gone
2518
+ * from the system, and there was nothing on screen to say so.
2519
+ *
2520
+ * Nothing here talks to a device or to the database, so it is testable on its
2521
+ * own — see `test/anpr-revoke.test.mjs`.
2522
+ */
2523
+ /** One camera's answer to "remove this record". */
2524
+ type AnprRevokeOutcome = {
2525
+ /** True ONLY when the device confirmed the removal, or held nothing to remove. */
2526
+ ok: boolean;
2527
+ /** HTTP status the device answered with, when it answered at all. */
2528
+ statusCode?: number;
2529
+ /** Why it failed, in words an operator can act on. Empty when `ok`. */
2530
+ reason: string;
2531
+ };
2532
+
2508
2533
  declare enum CameraType {
2509
2534
  IP = "ip",
2510
2535
  ANPR = "anpr"
@@ -2573,7 +2598,7 @@ declare function useDahuaService(): {
2573
2598
  password: string;
2574
2599
  mode: string;
2575
2600
  recno: string;
2576
- }) => Promise<urllib.HttpClientResponse<any> | undefined>;
2601
+ }) => Promise<AnprRevokeOutcome>;
2577
2602
  updatePlateNumber: (value: {
2578
2603
  host: string;
2579
2604
  username: string;
@@ -2592,6 +2617,18 @@ declare function useDahuaService(): {
2592
2617
  password: string;
2593
2618
  mode: string;
2594
2619
  plateNumber: string;
2620
+ /**
2621
+ * Treat a device that did not answer 200 as an error rather than as "the
2622
+ * plate is not on this camera".
2623
+ *
2624
+ * `useDahuaDigest` does not throw on a bad status, so a 401 or a 500 comes
2625
+ * back as an empty body, which `parseDahuaFind` reads as `found=0`. For the
2626
+ * visitor check-in callers that has always meant "not blocklisted" and is
2627
+ * left exactly as it is — a camera fault must not stop a visitor at the
2628
+ * gate. For the vehicle revoke paths it would mean "nothing to remove", and
2629
+ * they must not conclude that from a camera that never answered.
2630
+ */
2631
+ requireOk?: boolean;
2595
2632
  }) => Promise<any>;
2596
2633
  bulkInsertPlateNumber: (value: {
2597
2634
  host: string;
@@ -4109,7 +4146,7 @@ declare function useCameraViewService(): {
4109
4146
  userId?: string;
4110
4147
  }) => Promise<{
4111
4148
  reason: string;
4112
- verified: "recorder-video-port" | "nothing";
4149
+ verified: "nothing" | "recorder-video-port";
4113
4150
  verifiedDescription: string;
4114
4151
  cameraVerified: null;
4115
4152
  relayVerified: null;
@@ -4123,7 +4160,7 @@ declare function useCameraViewService(): {
4123
4160
  driftSeconds: null;
4124
4161
  detailUnavailableReason: string | null;
4125
4162
  } | {
4126
- verified: "recorder-video-port" | "nothing";
4163
+ verified: "nothing" | "recorder-video-port";
4127
4164
  verifiedDescription: string;
4128
4165
  cameraVerified: null;
4129
4166
  relayVerified: null;
@@ -4166,7 +4203,7 @@ declare function useCameraViewService(): {
4166
4203
  }) => Promise<{
4167
4204
  cameras: ({
4168
4205
  reason: string;
4169
- verified: "recorder-video-port" | "nothing";
4206
+ verified: "nothing" | "recorder-video-port";
4170
4207
  verifiedDescription: string;
4171
4208
  cameraVerified: null;
4172
4209
  relayVerified: null;
@@ -4180,7 +4217,7 @@ declare function useCameraViewService(): {
4180
4217
  lastFrameAt: string | null;
4181
4218
  unavailableReason: string | null;
4182
4219
  } | {
4183
- verified: "recorder-video-port" | "nothing";
4220
+ verified: "nothing" | "recorder-video-port";
4184
4221
  verifiedDescription: string;
4185
4222
  cameraVerified: null;
4186
4223
  relayVerified: null;
@@ -5011,7 +5048,10 @@ declare function usePersonRepo(): {
5011
5048
  plateNumber: string;
5012
5049
  recNo: string;
5013
5050
  }, session?: ClientSession) => Promise<void>;
5014
- pullVehicleByRecNo: (recNo: string, session?: ClientSession) => Promise<void>;
5051
+ pullVehicleByRecNo: (recNo: string, scope: {
5052
+ peopleId?: string | ObjectId;
5053
+ site?: string | ObjectId;
5054
+ }, session?: ClientSession) => Promise<void>;
5015
5055
  getByUserId: (userId: string | ObjectId) => Promise<TPerson | null>;
5016
5056
  reviewResidentPerson: (_id: ObjectId | string, value: Partial<TPerson>, session?: ClientSession) => Promise<mongodb.UpdateResult<TPerson>>;
5017
5057
  addFromVisitor: (value: visitorPersonRepo, session?: ClientSession) => Promise<mongodb.InsertOneResult<TPerson>>;
package/dist/index.js CHANGED
@@ -18186,11 +18186,23 @@ function usePersonRepo() {
18186
18186
  throw new import_node_server_utils41.InternalServerError("Failed to push vehicle plates by nric.");
18187
18187
  }
18188
18188
  }
18189
- async function pullVehicleByRecNo(recNo, session) {
18189
+ async function pullVehicleByRecNo(recNo, scope, session) {
18190
18190
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
18191
+ const owner = scope?.peopleId ? (0, import_node_server_utils41.toObjectId)(scope.peopleId) : null;
18192
+ const site = scope?.site ? (0, import_node_server_utils41.toObjectId)(scope.site) : null;
18193
+ if (!owner && !site) {
18194
+ throw new import_node_server_utils41.BadRequestError(
18195
+ "Cannot remove a vehicle plate without a person or a site."
18196
+ );
18197
+ }
18198
+ const filter = { "plates.recNo": recNo };
18199
+ if (owner)
18200
+ filter._id = owner;
18201
+ else
18202
+ filter.site = site;
18191
18203
  try {
18192
18204
  await collection.updateOne(
18193
- { "plates.recNo": recNo },
18205
+ filter,
18194
18206
  {
18195
18207
  $pull: {
18196
18208
  plates: {
@@ -22296,6 +22308,25 @@ function buildPlateRemoveEndpoint(value) {
22296
22308
  return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
22297
22309
  }
22298
22310
 
22311
+ // src/utils/anpr-revoke.util.ts
22312
+ function anprRevokeOutcome(response) {
22313
+ const statusCode = response?.statusCode ?? response?.status ?? response?.res?.status;
22314
+ if (statusCode === 200)
22315
+ return { ok: true, statusCode, reason: "" };
22316
+ if (statusCode === void 0) {
22317
+ return { ok: false, reason: "the camera did not answer" };
22318
+ }
22319
+ return { ok: false, statusCode, reason: `the camera answered ${statusCode}` };
22320
+ }
22321
+ function anprRevokeThrew(error) {
22322
+ const message = error?.message ?? String(error ?? "");
22323
+ return { ok: false, reason: message || "the camera could not be reached" };
22324
+ }
22325
+ function anprRevokeFailureMessage(failures, action = "deleted") {
22326
+ const detail = failures.map((failure) => `${failure.camera} (${failure.reason})`).join("; ");
22327
+ return `The plate number could not be removed from ${failures.length} ANPR camera(s): ${detail}. The vehicle has NOT been ${action} \u2014 it would still open the barrier. Check the camera, then try again.`;
22328
+ }
22329
+
22299
22330
  // src/services/dahua.service.ts
22300
22331
  function notifyCameraFault(camera, message) {
22301
22332
  NotificationService.cameraFault({
@@ -23187,9 +23218,10 @@ function useDahuaService() {
23187
23218
  mode: value.mode
23188
23219
  })
23189
23220
  });
23190
- return response;
23221
+ return anprRevokeOutcome(response);
23191
23222
  } catch (error2) {
23192
23223
  loggerDahua.error(`[${value.host}] Error removing plate number:`, error2);
23224
+ return anprRevokeThrew(error2);
23193
23225
  }
23194
23226
  }
23195
23227
  async function getPlateNumber(value) {
@@ -23198,7 +23230,8 @@ function useDahuaService() {
23198
23230
  username: import_joi24.default.string().required(),
23199
23231
  password: import_joi24.default.string().required(),
23200
23232
  plateNumber: import_joi24.default.string().required(),
23201
- mode: import_joi24.default.string().valid(...Object.values(ANPRMode)).required()
23233
+ mode: import_joi24.default.string().valid(...Object.values(ANPRMode)).required(),
23234
+ requireOk: import_joi24.default.boolean().optional()
23202
23235
  });
23203
23236
  const { error } = validation.validate(value);
23204
23237
  if (error) {
@@ -23211,6 +23244,12 @@ function useDahuaService() {
23211
23244
  password: value.password,
23212
23245
  endpoint: `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${encodeURIComponent(value.plateNumber)}`
23213
23246
  });
23247
+ if (value.requireOk) {
23248
+ const statusCode = getDahuaStatusCode(response);
23249
+ if (statusCode !== 200) {
23250
+ throw new Error(`the camera answered ${statusCode ?? "nothing"}`);
23251
+ }
23252
+ }
23214
23253
  const text2 = response?.data?.toString?.("utf-8");
23215
23254
  return text2;
23216
23255
  } catch (error2) {
@@ -35215,31 +35254,72 @@ function useVehicleService() {
35215
35254
  pages = siteCameraReq.pages || 1;
35216
35255
  siteCameras.push(...siteCameraReq.items);
35217
35256
  page++;
35218
- } while (page < pages);
35219
- if (!siteCameras.length) {
35220
- throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35221
- }
35257
+ } while (page <= pages);
35258
+ import_node_server_utils99.logger.info(
35259
+ `deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${site}`
35260
+ );
35261
+ const vehicle = await findOne({ _id: new import_mongodb67.ObjectId(_id) });
35262
+ const plateNumber = String(vehicle?.plateNumber ?? "");
35263
+ const failures = [];
35222
35264
  for (const camera of siteCameras) {
35223
35265
  const host = camera.host;
35224
35266
  const username = camera.username;
35225
35267
  const password = camera.password;
35226
- const dahuaPayload = {
35268
+ const label = String(
35269
+ camera?.name || camera?._id || "ANPR camera"
35270
+ );
35271
+ let recnoForCamera = recno;
35272
+ if (plateNumber) {
35273
+ try {
35274
+ const found = parseDahuaFind(
35275
+ await _getPlateNumber({
35276
+ host,
35277
+ username,
35278
+ password,
35279
+ mode: _mode,
35280
+ plateNumber,
35281
+ requireOk: true
35282
+ }) ?? ""
35283
+ );
35284
+ if (!found.exists)
35285
+ continue;
35286
+ if (found.recNo)
35287
+ recnoForCamera = found.recNo;
35288
+ } catch (error) {
35289
+ failures.push({
35290
+ camera: label,
35291
+ reason: anprRevokeThrew(error).reason
35292
+ });
35293
+ continue;
35294
+ }
35295
+ }
35296
+ const outcome = await _removePlateNumber({
35227
35297
  host,
35228
35298
  username,
35229
35299
  password,
35230
- recno,
35300
+ recno: recnoForCamera,
35231
35301
  mode: _mode
35232
- };
35233
- const dahuaResponse = await _removePlateNumber(dahuaPayload);
35234
- if (!bypass && dahuaResponse?.statusCode !== 200) {
35235
- throw new import_node_server_utils99.BadRequestError(
35236
- `Failed to remove plate number to ANPR from ${type}`
35237
- );
35302
+ });
35303
+ if (!outcome?.ok) {
35304
+ failures.push({
35305
+ camera: label,
35306
+ reason: outcome?.reason || "the camera did not answer"
35307
+ });
35238
35308
  }
35239
35309
  }
35310
+ if (failures.length) {
35311
+ const message = anprRevokeFailureMessage(failures);
35312
+ if (!bypass)
35313
+ throw new import_node_server_utils99.BadRequestError(message);
35314
+ import_node_server_utils99.logger.warn(`deleteVehicle bypassed a failed ANPR revoke: ${message}`);
35315
+ }
35240
35316
  await Promise.all([
35241
35317
  _deleteVehicle(_id, session),
35242
- _pullVehicleByRecNo(recno, session)
35318
+ _pullVehicleByRecNo(
35319
+ recno,
35320
+ { peopleId: vehicle?.peopleId, site },
35321
+ session
35322
+ )
35243
35323
  ]);
35244
35324
  await session.commitTransaction();
35245
35325
  return `Vehicle plate number deleted from ${type} record successfully.`;
@@ -35303,7 +35383,7 @@ function useVehicleService() {
35303
35383
  pages = siteCameraReq.pages || 1;
35304
35384
  siteCameras.push(...siteCameraReq.items);
35305
35385
  page++;
35306
- } while (page < pages);
35386
+ } while (page <= pages);
35307
35387
  if (!siteCameras.length) {
35308
35388
  throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35309
35389
  }
@@ -35382,7 +35462,7 @@ function useVehicleService() {
35382
35462
  pages = siteCameraReq.pages || 1;
35383
35463
  siteCameras.push(...siteCameraReq.items);
35384
35464
  page++;
35385
- } while (page < pages);
35465
+ } while (page <= pages);
35386
35466
  if (!siteCameras.length) {
35387
35467
  throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35388
35468
  }
@@ -35460,7 +35540,7 @@ function useVehicleService() {
35460
35540
  pages = siteCameraReq.pages || 1;
35461
35541
  siteCameras.push(...siteCameraReq.items);
35462
35542
  page++;
35463
- } while (page < pages);
35543
+ } while (page <= pages);
35464
35544
  if (!siteCameras.length) {
35465
35545
  throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35466
35546
  }
@@ -35549,7 +35629,7 @@ function useVehicleService() {
35549
35629
  pages = siteCameraReq.pages || 1;
35550
35630
  siteCameras.push(...siteCameraReq.items);
35551
35631
  page++;
35552
- } while (page < pages);
35632
+ } while (page <= pages);
35553
35633
  if (!siteCameras.length) {
35554
35634
  throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35555
35635
  }
@@ -35604,6 +35684,37 @@ function useVehicleService() {
35604
35684
  }
35605
35685
  } else {
35606
35686
  console.log("updateVehicleById _plateNumber == plateNumber", _plateNumber, plateNumber);
35687
+ const existing = parseDahuaFind(
35688
+ await _getPlateNumber({
35689
+ host,
35690
+ username,
35691
+ password,
35692
+ mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
35693
+ plateNumber,
35694
+ requireOk: true
35695
+ }) ?? ""
35696
+ );
35697
+ if (existing.exists) {
35698
+ const outcome = await _removePlateNumber({
35699
+ host,
35700
+ username,
35701
+ password,
35702
+ mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
35703
+ recno: existing.recNo || _recNo
35704
+ });
35705
+ if (!outcome?.ok) {
35706
+ throw new import_node_server_utils99.BadRequestError(
35707
+ anprRevokeFailureMessage([
35708
+ {
35709
+ camera: String(
35710
+ camera?.name || camera?._id || "ANPR camera"
35711
+ ),
35712
+ reason: outcome?.reason || "the camera did not answer"
35713
+ }
35714
+ ], "updated")
35715
+ );
35716
+ }
35717
+ }
35607
35718
  const dahuaPayload = {
35608
35719
  host,
35609
35720
  username,
@@ -35785,7 +35896,7 @@ function useVehicleService() {
35785
35896
  pages = siteCameraReq.pages || 1;
35786
35897
  siteCameras.push(...siteCameraReq.items);
35787
35898
  page++;
35788
- } while (page < pages);
35899
+ } while (page <= pages);
35789
35900
  if (!siteCameras.length) {
35790
35901
  throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35791
35902
  }
@@ -35877,7 +35988,7 @@ function useVehicleService() {
35877
35988
  pages = siteCameraReq.pages || 1;
35878
35989
  siteCameras.push(...siteCameraReq.items);
35879
35990
  page++;
35880
- } while (page < pages);
35991
+ } while (page <= pages);
35881
35992
  if (!siteCameras.length) {
35882
35993
  throw new import_node_server_utils99.BadRequestError("No site cameras found.");
35883
35994
  }
@@ -39223,10 +39334,18 @@ function useVehicleController() {
39223
39334
  getBlocklistedVehicles: _getBlocklistedVehicles,
39224
39335
  getVehicleSite: _getVehicleSite
39225
39336
  } = useVehicleRepo();
39226
- async function requireVehicleReach(req, _id) {
39337
+ async function requireVehicleReach(req, _id, permission) {
39227
39338
  const vehicle = await _getVehicleSite(_id);
39228
- await requireSiteReach(req, vehicle?.site);
39339
+ await requireSiteReach(req, vehicle?.site, permission);
39229
39340
  }
39341
+ const VEHICLE_UPDATE = {
39342
+ permissions: ["vehicle-mgmt:update-vehicle"],
39343
+ message: "You do not have permission to edit vehicles."
39344
+ };
39345
+ const VEHICLE_DELETE = {
39346
+ permissions: ["vehicle-mgmt:delete-vehicle"],
39347
+ message: "You do not have permission to delete vehicles."
39348
+ };
39230
39349
  function normalizeRow(row) {
39231
39350
  return Object.fromEntries(
39232
39351
  Object.entries(row).map(([key, value]) => [
@@ -39595,7 +39714,7 @@ function useVehicleController() {
39595
39714
  return;
39596
39715
  }
39597
39716
  const { _id, ...rest } = value;
39598
- await requireVehicleReach(req, _id);
39717
+ await requireVehicleReach(req, _id, VEHICLE_UPDATE);
39599
39718
  await _updateVehicleById(_id, rest);
39600
39719
  res.json({ message: "Successfully updated vehicle." });
39601
39720
  return;
@@ -39617,7 +39736,11 @@ function useVehicleController() {
39617
39736
  recno: import_joi55.default.string().required(),
39618
39737
  site: import_joi55.default.string().hex().length(24).required(),
39619
39738
  type: import_joi55.default.string().valid(...Object.values(VehicleType)).required(),
39620
- bypass: import_joi55.default.boolean().optional().default(true)
39739
+ // Fail closed. Defaulted to TRUE, and neither the web app nor the MA
39740
+ // app sends the field, so EVERY delete skipped the camera-revoke guard
39741
+ // and reported success whatever the cameras answered. Bypassing is only
39742
+ // ever honoured now when a caller asks for it explicitly.
39743
+ bypass: import_joi55.default.boolean().optional().default(false)
39621
39744
  });
39622
39745
  const { error, value } = deleteVehicleSchema.validate({
39623
39746
  _id: req.params.id,
@@ -39631,7 +39754,7 @@ function useVehicleController() {
39631
39754
  }
39632
39755
  const { recno, site, type, bypass, _id } = value;
39633
39756
  try {
39634
- await requireVehicleReach(req, _id);
39757
+ await requireVehicleReach(req, _id, VEHICLE_DELETE);
39635
39758
  const data = await _deleteVehicle(_id, recno, site, type, bypass);
39636
39759
  res.json({
39637
39760
  message: "Vehicle deleted successfully.",
@@ -39946,7 +40069,7 @@ function useSiteCameraService() {
39946
40069
  pages = siteCameraReq.pages || 1;
39947
40070
  siteCameras.push(...siteCameraReq.items);
39948
40071
  page++;
39949
- } while (page < pages);
40072
+ } while (page <= pages);
39950
40073
  if (!siteCameras.length) {
39951
40074
  throw new import_node_server_utils114.BadRequestError("No ANPR cameras found.");
39952
40075
  }