@7365admin1/core 3.67.0 → 3.68.1
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.
- package/CHANGELOG.md +88 -1
- package/dist/index.d.ts +113 -6
- package/dist/index.js +2115 -866
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2524 -1273
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/anpr-plate.test.mjs +272 -0
- package/test/dahua-protocol.util.test.mjs +137 -0
- package/test/module-access.test.mjs +27 -0
- package/test/module-gate-kill-switch.test.mjs +48 -0
- package/test/people-create-gate.test.mjs +71 -0
- package/test/socket-session.test.mjs +119 -0
package/dist/index.js
CHANGED
|
@@ -536,13 +536,13 @@ var require_logger = __commonJS({
|
|
|
536
536
|
"use strict";
|
|
537
537
|
exports.__esModule = true;
|
|
538
538
|
var _utils = require_utils();
|
|
539
|
-
var
|
|
539
|
+
var logger227 = {
|
|
540
540
|
methodMap: ["debug", "info", "warn", "error"],
|
|
541
541
|
level: "info",
|
|
542
542
|
// Maps a given level value to the `methodMap` indexes above.
|
|
543
543
|
lookupLevel: function lookupLevel(level) {
|
|
544
544
|
if (typeof level === "string") {
|
|
545
|
-
var levelMap = _utils.indexOf(
|
|
545
|
+
var levelMap = _utils.indexOf(logger227.methodMap, level.toLowerCase());
|
|
546
546
|
if (levelMap >= 0) {
|
|
547
547
|
level = levelMap;
|
|
548
548
|
} else {
|
|
@@ -553,9 +553,9 @@ var require_logger = __commonJS({
|
|
|
553
553
|
},
|
|
554
554
|
// Can be overridden in the host environment
|
|
555
555
|
log: function log(level) {
|
|
556
|
-
level =
|
|
557
|
-
if (typeof console !== "undefined" &&
|
|
558
|
-
var method =
|
|
556
|
+
level = logger227.lookupLevel(level);
|
|
557
|
+
if (typeof console !== "undefined" && logger227.lookupLevel(logger227.level) <= level) {
|
|
558
|
+
var method = logger227.methodMap[level];
|
|
559
559
|
if (!console[method]) {
|
|
560
560
|
method = "log";
|
|
561
561
|
}
|
|
@@ -566,7 +566,7 @@ var require_logger = __commonJS({
|
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
568
|
};
|
|
569
|
-
exports["default"] =
|
|
569
|
+
exports["default"] = logger227;
|
|
570
570
|
module2.exports = exports["default"];
|
|
571
571
|
}
|
|
572
572
|
});
|
|
@@ -6145,6 +6145,7 @@ __export(src_exports, {
|
|
|
6145
6145
|
cameraGrant: () => cameraGrant,
|
|
6146
6146
|
cameraHealthClaim: () => cameraHealthClaim,
|
|
6147
6147
|
cameraHealthSummary: () => cameraHealthSummary,
|
|
6148
|
+
cameraListClearConfirmation: () => cameraListClearConfirmation,
|
|
6148
6149
|
cameraManagePermissions: () => cameraManagePermissions,
|
|
6149
6150
|
cameraProbeCacheKey: () => cameraProbeCacheKey,
|
|
6150
6151
|
cameraTransports: () => cameraTransports,
|
|
@@ -15844,6 +15845,8 @@ function deniedForMembership({
|
|
|
15844
15845
|
return { denied, since: [orgTier.since, siteTier.since].sort().pop() ?? "" };
|
|
15845
15846
|
}
|
|
15846
15847
|
function mustAcknowledge(preview, acknowledged) {
|
|
15848
|
+
if (!moduleListGateOn())
|
|
15849
|
+
return false;
|
|
15847
15850
|
if (acknowledged === true || !preview.newlyDenied.length)
|
|
15848
15851
|
return false;
|
|
15849
15852
|
return !preview.impact || preview.impact.memberCount > 0;
|
|
@@ -21858,6 +21861,7 @@ var import_fs2 = require("fs");
|
|
|
21858
21861
|
var import_joi26 = __toESM(require("joi"));
|
|
21859
21862
|
var path2 = __toESM(require("path"));
|
|
21860
21863
|
var import_urllib = require("urllib");
|
|
21864
|
+
var import_crypto2 = require("crypto");
|
|
21861
21865
|
|
|
21862
21866
|
// src/repositories/visitor-transaction.repo.ts
|
|
21863
21867
|
var import_node_server_utils56 = require("@7365admin1/node-server-utils");
|
|
@@ -24358,6 +24362,69 @@ function useVehicleRepo() {
|
|
|
24358
24362
|
throw error;
|
|
24359
24363
|
}
|
|
24360
24364
|
}
|
|
24365
|
+
async function getLivePlatesBySite(site) {
|
|
24366
|
+
let siteId;
|
|
24367
|
+
try {
|
|
24368
|
+
siteId = new import_mongodb47.ObjectId(site);
|
|
24369
|
+
} catch (error) {
|
|
24370
|
+
throw new import_node_server_utils58.BadRequestError("Invalid site ID format.");
|
|
24371
|
+
}
|
|
24372
|
+
try {
|
|
24373
|
+
return await collection.aggregate([
|
|
24374
|
+
{
|
|
24375
|
+
$match: {
|
|
24376
|
+
site: siteId,
|
|
24377
|
+
status: "active" /* ACTIVE */,
|
|
24378
|
+
plateNumber: { $nin: [null, ""] }
|
|
24379
|
+
}
|
|
24380
|
+
},
|
|
24381
|
+
{ $sort: { name: 1, plateNumber: 1 } },
|
|
24382
|
+
{
|
|
24383
|
+
$lookup: {
|
|
24384
|
+
from: "buildings",
|
|
24385
|
+
localField: "block",
|
|
24386
|
+
foreignField: "_id",
|
|
24387
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
24388
|
+
as: "blockDoc"
|
|
24389
|
+
}
|
|
24390
|
+
},
|
|
24391
|
+
{
|
|
24392
|
+
$lookup: {
|
|
24393
|
+
from: "building-levels",
|
|
24394
|
+
localField: "level",
|
|
24395
|
+
foreignField: "_id",
|
|
24396
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
24397
|
+
as: "levelDoc"
|
|
24398
|
+
}
|
|
24399
|
+
},
|
|
24400
|
+
{
|
|
24401
|
+
$lookup: {
|
|
24402
|
+
from: "building-units",
|
|
24403
|
+
localField: "unit",
|
|
24404
|
+
foreignField: "_id",
|
|
24405
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
24406
|
+
as: "unitDoc"
|
|
24407
|
+
}
|
|
24408
|
+
},
|
|
24409
|
+
{
|
|
24410
|
+
$project: {
|
|
24411
|
+
plateNumber: 1,
|
|
24412
|
+
name: 1,
|
|
24413
|
+
start: 1,
|
|
24414
|
+
end: 1,
|
|
24415
|
+
type: 1,
|
|
24416
|
+
blockedFrom: 1,
|
|
24417
|
+
seasonPassType: 1,
|
|
24418
|
+
blockName: { $ifNull: [{ $arrayElemAt: ["$blockDoc.name", 0] }, ""] },
|
|
24419
|
+
levelName: { $ifNull: [{ $arrayElemAt: ["$levelDoc.name", 0] }, ""] },
|
|
24420
|
+
unitName: { $ifNull: [{ $arrayElemAt: ["$unitDoc.name", 0] }, ""] }
|
|
24421
|
+
}
|
|
24422
|
+
}
|
|
24423
|
+
]).toArray();
|
|
24424
|
+
} catch (error) {
|
|
24425
|
+
throw new import_node_server_utils58.InternalServerError("Failed to list the site's vehicles.");
|
|
24426
|
+
}
|
|
24427
|
+
}
|
|
24361
24428
|
async function getAllExpiredVehicles() {
|
|
24362
24429
|
try {
|
|
24363
24430
|
const items = await collection.find(expiredVehicleSweepFilter()).toArray();
|
|
@@ -24562,6 +24629,7 @@ function useVehicleRepo() {
|
|
|
24562
24629
|
getVehicleByPlateNumber,
|
|
24563
24630
|
getVehiclesByNRIC,
|
|
24564
24631
|
deleteExpiredVehicles,
|
|
24632
|
+
getLivePlatesBySite,
|
|
24565
24633
|
getAllVehiclesByUnitId,
|
|
24566
24634
|
getAllExpiredVehicles,
|
|
24567
24635
|
bulkUpsertVehicles,
|
|
@@ -24761,6 +24829,73 @@ function classifyDahuaFailure(message) {
|
|
|
24761
24829
|
}
|
|
24762
24830
|
return "transient";
|
|
24763
24831
|
}
|
|
24832
|
+
var DAHUA_COMMAND_ATTEMPTS = 10;
|
|
24833
|
+
var DAHUA_COMMAND_DEADLINE_MS = 45e3;
|
|
24834
|
+
var DAHUA_LOGIN_PROVEN_FOR_MS = 10 * 6e4;
|
|
24835
|
+
var COMMAND_RETRY_DELAYS_MS = [1e3, 2e3, 3e3, 4e3, 5e3];
|
|
24836
|
+
function dahuaCommandRetryDelayMs(failedAttempt) {
|
|
24837
|
+
const index = Math.min(
|
|
24838
|
+
Math.max(Math.floor(failedAttempt), 1),
|
|
24839
|
+
COMMAND_RETRY_DELAYS_MS.length
|
|
24840
|
+
);
|
|
24841
|
+
return COMMAND_RETRY_DELAYS_MS[index - 1];
|
|
24842
|
+
}
|
|
24843
|
+
var NOT_SENT_PATTERN = /ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ENOTFOUND|UND_ERR_CONNECT_TIMEOUT|connect ?timeout|ConnectTimeout/i;
|
|
24844
|
+
function classifyDahuaAttempt(attempt) {
|
|
24845
|
+
if (attempt.error !== void 0 && attempt.error !== null) {
|
|
24846
|
+
const error = attempt.error;
|
|
24847
|
+
const message = [error?.name, error?.code, error?.cause?.code, error?.message ?? error].filter(Boolean).join(" ");
|
|
24848
|
+
if (/invalid authority|\b401\b|unauthori[sz]ed/i.test(message)) {
|
|
24849
|
+
return "login-refused";
|
|
24850
|
+
}
|
|
24851
|
+
if (/\b403\b|forbidden/i.test(message))
|
|
24852
|
+
return "forbidden";
|
|
24853
|
+
if (NOT_SENT_PATTERN.test(message))
|
|
24854
|
+
return "not-sent";
|
|
24855
|
+
return "maybe-sent";
|
|
24856
|
+
}
|
|
24857
|
+
const status = attempt.statusCode;
|
|
24858
|
+
if (status === 401 || /invalid authority/i.test(String(attempt.text ?? ""))) {
|
|
24859
|
+
return "login-refused";
|
|
24860
|
+
}
|
|
24861
|
+
if (status === 403)
|
|
24862
|
+
return "forbidden";
|
|
24863
|
+
if (status === 502 || status === 503 || status === 504)
|
|
24864
|
+
return "busy";
|
|
24865
|
+
if (status === void 0)
|
|
24866
|
+
return "maybe-sent";
|
|
24867
|
+
return "answered";
|
|
24868
|
+
}
|
|
24869
|
+
function shouldRetryDahuaCommand(options) {
|
|
24870
|
+
const {
|
|
24871
|
+
kind,
|
|
24872
|
+
attempt,
|
|
24873
|
+
loginFailures,
|
|
24874
|
+
loginProven,
|
|
24875
|
+
repeatable,
|
|
24876
|
+
elapsedMs,
|
|
24877
|
+
nextDelayMs,
|
|
24878
|
+
maxAttempts = DAHUA_COMMAND_ATTEMPTS,
|
|
24879
|
+
deadlineMs = DAHUA_COMMAND_DEADLINE_MS
|
|
24880
|
+
} = options;
|
|
24881
|
+
if (kind === "answered" || kind === "forbidden")
|
|
24882
|
+
return false;
|
|
24883
|
+
if (attempt >= maxAttempts)
|
|
24884
|
+
return false;
|
|
24885
|
+
if (elapsedMs + nextDelayMs > deadlineMs)
|
|
24886
|
+
return false;
|
|
24887
|
+
switch (kind) {
|
|
24888
|
+
case "login-refused":
|
|
24889
|
+
return loginProven || loginFailures < DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT;
|
|
24890
|
+
case "busy":
|
|
24891
|
+
case "not-sent":
|
|
24892
|
+
return true;
|
|
24893
|
+
case "maybe-sent":
|
|
24894
|
+
return repeatable;
|
|
24895
|
+
default:
|
|
24896
|
+
return false;
|
|
24897
|
+
}
|
|
24898
|
+
}
|
|
24764
24899
|
var enc = (value) => encodeURIComponent(String(value));
|
|
24765
24900
|
function buildPlateUpdateEndpoint(value) {
|
|
24766
24901
|
return `/cgi-bin/recordUpdater.cgi?action=update&name=${enc(value.mode)}&recno=${enc(value.recno)}&PlateNumber=${enc(value.plateNumber)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
|
|
@@ -24771,6 +24906,13 @@ function buildPlateInsertEndpoint(value) {
|
|
|
24771
24906
|
function buildPlateRemoveEndpoint(value) {
|
|
24772
24907
|
return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
|
|
24773
24908
|
}
|
|
24909
|
+
var CLEARABLE_PLATE_LISTS = ["TrafficRedList", "TrafficBlackList"];
|
|
24910
|
+
function buildPlateClearEndpoint(value) {
|
|
24911
|
+
if (!CLEARABLE_PLATE_LISTS.includes(value.mode)) {
|
|
24912
|
+
throw new Error(`Refusing to clear record set ${JSON.stringify(value.mode)}.`);
|
|
24913
|
+
}
|
|
24914
|
+
return `/cgi-bin/recordUpdater.cgi?action=clear&name=${enc(value.mode)}`;
|
|
24915
|
+
}
|
|
24774
24916
|
|
|
24775
24917
|
// src/utils/anpr-revoke.util.ts
|
|
24776
24918
|
function anprRevokeOutcome(response) {
|
|
@@ -24896,6 +25038,148 @@ function anprNoCameraRefusal(outcome) {
|
|
|
24896
25038
|
return `There is no ANPR camera set up at this site, so the plate cannot be registered on a barrier. The vehicle has NOT been ${outcome}. Add a camera in Settings first, then try again.`;
|
|
24897
25039
|
}
|
|
24898
25040
|
|
|
25041
|
+
// src/utils/anpr-plate.util.ts
|
|
25042
|
+
var ALLOW_CAMERA_DIRECTIONS = ["entry", "both", "residents"];
|
|
25043
|
+
var BLOCK_CAMERA_DIRECTIONS = ["entry", "both", "residents", "visitors"];
|
|
25044
|
+
function isAllowCameraDirection(direction) {
|
|
25045
|
+
return ALLOW_CAMERA_DIRECTIONS.includes(
|
|
25046
|
+
String(direction ?? "").trim().toLowerCase()
|
|
25047
|
+
);
|
|
25048
|
+
}
|
|
25049
|
+
function isBlockCameraDirection(direction) {
|
|
25050
|
+
return BLOCK_CAMERA_DIRECTIONS.includes(
|
|
25051
|
+
String(direction ?? "").trim().toLowerCase()
|
|
25052
|
+
);
|
|
25053
|
+
}
|
|
25054
|
+
function listsFilledByDirectionChange(from, to) {
|
|
25055
|
+
const before = String(from ?? "").trim().toLowerCase();
|
|
25056
|
+
const after = String(to ?? "").trim().toLowerCase();
|
|
25057
|
+
if (!after || after === before)
|
|
25058
|
+
return { allow: false, block: false };
|
|
25059
|
+
return {
|
|
25060
|
+
allow: isAllowCameraDirection(after) && !isAllowCameraDirection(before),
|
|
25061
|
+
block: isBlockCameraDirection(after) && !isBlockCameraDirection(before)
|
|
25062
|
+
};
|
|
25063
|
+
}
|
|
25064
|
+
function listsClearedByDirectionChange(from, to) {
|
|
25065
|
+
const before = String(from ?? "").trim().toLowerCase();
|
|
25066
|
+
const after = String(to ?? "").trim().toLowerCase();
|
|
25067
|
+
if (!after || after === before)
|
|
25068
|
+
return { allow: false, block: false };
|
|
25069
|
+
if (after === "exit")
|
|
25070
|
+
return { allow: true, block: true };
|
|
25071
|
+
return {
|
|
25072
|
+
allow: isAllowCameraDirection(before) && !isAllowCameraDirection(after),
|
|
25073
|
+
block: false
|
|
25074
|
+
};
|
|
25075
|
+
}
|
|
25076
|
+
function mustRevokeFrom(direction, list, vehicleIsBlocked = false) {
|
|
25077
|
+
if (list === "allowed")
|
|
25078
|
+
return isAllowCameraDirection(direction);
|
|
25079
|
+
if (isBlockCameraDirection(direction))
|
|
25080
|
+
return true;
|
|
25081
|
+
return vehicleIsBlocked;
|
|
25082
|
+
}
|
|
25083
|
+
function normalisePlate(plate) {
|
|
25084
|
+
return String(plate ?? "").replace(/\s+/g, "").toUpperCase();
|
|
25085
|
+
}
|
|
25086
|
+
function samePlate(a, b) {
|
|
25087
|
+
const left = normalisePlate(a);
|
|
25088
|
+
return left !== "" && left === normalisePlate(b);
|
|
25089
|
+
}
|
|
25090
|
+
function parseDahuaFindRecords(text2) {
|
|
25091
|
+
const body = String(text2 ?? "");
|
|
25092
|
+
const found = Number(body.match(/found=(\d+)/i)?.[1] || 0);
|
|
25093
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
25094
|
+
const pattern = /records\[(\d+)\]\.(RecNo|PlateNumber|BeginTime|CancelTime|MasterOfCar)=([^\r\n]*)/gi;
|
|
25095
|
+
for (const match of body.matchAll(pattern)) {
|
|
25096
|
+
const index = Number(match[1]);
|
|
25097
|
+
const key = match[2].toLowerCase();
|
|
25098
|
+
const value = match[3].trim();
|
|
25099
|
+
const record = byIndex.get(index) ?? { recNo: "", plateNumber: "" };
|
|
25100
|
+
if (key === "recno")
|
|
25101
|
+
record.recNo = value;
|
|
25102
|
+
else if (key === "platenumber")
|
|
25103
|
+
record.plateNumber = value;
|
|
25104
|
+
else if (key === "begintime")
|
|
25105
|
+
record.beginTime = value;
|
|
25106
|
+
else if (key === "canceltime")
|
|
25107
|
+
record.cancelTime = value;
|
|
25108
|
+
else
|
|
25109
|
+
record.owner = value;
|
|
25110
|
+
byIndex.set(index, record);
|
|
25111
|
+
}
|
|
25112
|
+
const records = Array.from(byIndex.entries()).sort(([a], [b]) => a - b).map(([, record]) => record).filter((record) => record.recNo !== "");
|
|
25113
|
+
return { found, records };
|
|
25114
|
+
}
|
|
25115
|
+
function matchingPlateRecords(text2, plateNumber) {
|
|
25116
|
+
return parseDahuaFindRecords(text2).records.filter(
|
|
25117
|
+
(record) => record.plateNumber === "" || samePlate(record.plateNumber, plateNumber)
|
|
25118
|
+
);
|
|
25119
|
+
}
|
|
25120
|
+
function listsHeldBy(vehicle) {
|
|
25121
|
+
const type = String(vehicle?.type ?? "");
|
|
25122
|
+
return {
|
|
25123
|
+
allow: type === "whitelist" || String(vehicle?.blockedFrom ?? "") === "whitelist",
|
|
25124
|
+
block: type === "blocklist"
|
|
25125
|
+
};
|
|
25126
|
+
}
|
|
25127
|
+
function listsToRevoke(othersStillLive) {
|
|
25128
|
+
const others = (othersStillLive ?? []).map(listsHeldBy);
|
|
25129
|
+
return {
|
|
25130
|
+
allow: !others.some((held) => held.allow),
|
|
25131
|
+
block: !others.some((held) => held.block)
|
|
25132
|
+
};
|
|
25133
|
+
}
|
|
25134
|
+
var DEFAULT_CAMERA_TIMEZONE = "Asia/Singapore";
|
|
25135
|
+
function usableTimezone(timezone) {
|
|
25136
|
+
const zone = String(timezone ?? "").trim();
|
|
25137
|
+
if (!zone)
|
|
25138
|
+
return DEFAULT_CAMERA_TIMEZONE;
|
|
25139
|
+
try {
|
|
25140
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
25141
|
+
return zone;
|
|
25142
|
+
} catch {
|
|
25143
|
+
return DEFAULT_CAMERA_TIMEZONE;
|
|
25144
|
+
}
|
|
25145
|
+
}
|
|
25146
|
+
var WALL_CLOCK = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
25147
|
+
function dahuaWallClock(value, timezone) {
|
|
25148
|
+
if (value === null || value === void 0 || value === "")
|
|
25149
|
+
return null;
|
|
25150
|
+
if (typeof value === "string") {
|
|
25151
|
+
const wall = value.trim().match(WALL_CLOCK);
|
|
25152
|
+
if (wall) {
|
|
25153
|
+
const [, y, mo, d, h, mi, se] = wall;
|
|
25154
|
+
return `${y}-${mo}-${d} ${h}:${mi}:${se ?? "00"}`;
|
|
25155
|
+
}
|
|
25156
|
+
}
|
|
25157
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
25158
|
+
if (Number.isNaN(date.getTime()))
|
|
25159
|
+
return null;
|
|
25160
|
+
const parts = new Intl.DateTimeFormat("en-GB", {
|
|
25161
|
+
timeZone: usableTimezone(timezone),
|
|
25162
|
+
year: "numeric",
|
|
25163
|
+
month: "2-digit",
|
|
25164
|
+
day: "2-digit",
|
|
25165
|
+
hour: "2-digit",
|
|
25166
|
+
minute: "2-digit",
|
|
25167
|
+
second: "2-digit",
|
|
25168
|
+
hourCycle: "h23"
|
|
25169
|
+
}).formatToParts(date);
|
|
25170
|
+
const part = (type) => parts.find((entry) => entry.type === type)?.value ?? "00";
|
|
25171
|
+
return `${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")}:${part("second")}`;
|
|
25172
|
+
}
|
|
25173
|
+
function plateWindowMatches(record, wanted) {
|
|
25174
|
+
const same = (held, want) => {
|
|
25175
|
+
const a = dahuaWallClock(held);
|
|
25176
|
+
return a !== null && a === dahuaWallClock(want);
|
|
25177
|
+
};
|
|
25178
|
+
if (!wanted.start || !wanted.end)
|
|
25179
|
+
return false;
|
|
25180
|
+
return same(record.beginTime, wanted.start) && same(record.cancelTime, wanted.end);
|
|
25181
|
+
}
|
|
25182
|
+
|
|
24899
25183
|
// src/services/dahua.service.ts
|
|
24900
25184
|
function notifyCameraFault(camera, message) {
|
|
24901
25185
|
NotificationService.cameraFault({
|
|
@@ -24967,12 +25251,14 @@ async function useDahuaDigestWithRetry({
|
|
|
24967
25251
|
const statusCode = getDahuaStatusCode(response);
|
|
24968
25252
|
if (streaming) {
|
|
24969
25253
|
if (statusCode === 200) {
|
|
25254
|
+
markLoginProven(host, username, password);
|
|
24970
25255
|
return response;
|
|
24971
25256
|
}
|
|
24972
25257
|
} else {
|
|
24973
25258
|
const text2 = getDahuaResponseText(response);
|
|
24974
25259
|
const isOk = statusCode === 200 && text2.includes("OK");
|
|
24975
25260
|
if (isOk) {
|
|
25261
|
+
markLoginProven(host, username, password);
|
|
24976
25262
|
return response;
|
|
24977
25263
|
}
|
|
24978
25264
|
}
|
|
@@ -25006,6 +25292,123 @@ async function useDahuaDigestWithRetry({
|
|
|
25006
25292
|
}
|
|
25007
25293
|
throw lastError;
|
|
25008
25294
|
}
|
|
25295
|
+
var provenLogins = /* @__PURE__ */ new Map();
|
|
25296
|
+
function loginKey(host, username, password) {
|
|
25297
|
+
return (0, import_crypto2.createHash)("sha256").update(`${host ?? ""}
|
|
25298
|
+
${username ?? ""}
|
|
25299
|
+
${password ?? ""}`).digest("hex");
|
|
25300
|
+
}
|
|
25301
|
+
function markLoginProven(host, username, password) {
|
|
25302
|
+
provenLogins.set(loginKey(host, username, password), Date.now());
|
|
25303
|
+
}
|
|
25304
|
+
function forgetLogin(host, username, password) {
|
|
25305
|
+
provenLogins.delete(loginKey(host, username, password));
|
|
25306
|
+
}
|
|
25307
|
+
function isLoginProven(host, username, password) {
|
|
25308
|
+
const at2 = provenLogins.get(loginKey(host, username, password));
|
|
25309
|
+
return at2 !== void 0 && Date.now() - at2 < DAHUA_LOGIN_PROVEN_FOR_MS;
|
|
25310
|
+
}
|
|
25311
|
+
var ATTEMPT_WORDS = {
|
|
25312
|
+
"login-refused": "login refused",
|
|
25313
|
+
forbidden: "credentials refused",
|
|
25314
|
+
busy: "camera busy",
|
|
25315
|
+
"not-sent": "could not connect",
|
|
25316
|
+
"maybe-sent": "connection lost"
|
|
25317
|
+
};
|
|
25318
|
+
async function sendCameraCommand({
|
|
25319
|
+
host,
|
|
25320
|
+
username,
|
|
25321
|
+
password,
|
|
25322
|
+
endpoint,
|
|
25323
|
+
timeout = 1e4,
|
|
25324
|
+
repeatable = true,
|
|
25325
|
+
label = "command",
|
|
25326
|
+
deadlineMs = DAHUA_COMMAND_DEADLINE_MS
|
|
25327
|
+
}) {
|
|
25328
|
+
const startedAt = Date.now();
|
|
25329
|
+
let loginFailures = 0;
|
|
25330
|
+
for (let attempt = 1; ; attempt++) {
|
|
25331
|
+
let response;
|
|
25332
|
+
let error;
|
|
25333
|
+
try {
|
|
25334
|
+
response = await useDahuaDigest({
|
|
25335
|
+
host,
|
|
25336
|
+
username,
|
|
25337
|
+
password,
|
|
25338
|
+
endpoint,
|
|
25339
|
+
headers: { Connection: "close" },
|
|
25340
|
+
timeout
|
|
25341
|
+
});
|
|
25342
|
+
} catch (thrown) {
|
|
25343
|
+
error = thrown ?? new Error("the request failed with no error");
|
|
25344
|
+
}
|
|
25345
|
+
const kind = classifyDahuaAttempt(
|
|
25346
|
+
error ? { error } : {
|
|
25347
|
+
statusCode: getDahuaStatusCode(response),
|
|
25348
|
+
text: getDahuaResponseText(response)
|
|
25349
|
+
}
|
|
25350
|
+
);
|
|
25351
|
+
if (kind === "answered") {
|
|
25352
|
+
markLoginProven(host, username, password);
|
|
25353
|
+
if (attempt > 1) {
|
|
25354
|
+
loggerDahua.info(`[${host}] ${label}: went through on try ${attempt}`);
|
|
25355
|
+
}
|
|
25356
|
+
return response;
|
|
25357
|
+
}
|
|
25358
|
+
if (kind === "login-refused")
|
|
25359
|
+
loginFailures++;
|
|
25360
|
+
if (kind === "forbidden")
|
|
25361
|
+
forgetLogin(host, username, password);
|
|
25362
|
+
const nextDelayMs = dahuaCommandRetryDelayMs(attempt);
|
|
25363
|
+
const retry = shouldRetryDahuaCommand({
|
|
25364
|
+
kind,
|
|
25365
|
+
attempt,
|
|
25366
|
+
loginFailures,
|
|
25367
|
+
loginProven: isLoginProven(host, username, password),
|
|
25368
|
+
repeatable,
|
|
25369
|
+
elapsedMs: Date.now() - startedAt,
|
|
25370
|
+
nextDelayMs,
|
|
25371
|
+
deadlineMs
|
|
25372
|
+
});
|
|
25373
|
+
const why = ATTEMPT_WORDS[kind] ?? kind;
|
|
25374
|
+
if (!retry) {
|
|
25375
|
+
if (attempt > 1 || kind !== "forbidden") {
|
|
25376
|
+
loggerDahua.warn(
|
|
25377
|
+
`[${host}] ${label}: ${why} on try ${attempt}/${DAHUA_COMMAND_ATTEMPTS}; giving up`
|
|
25378
|
+
);
|
|
25379
|
+
}
|
|
25380
|
+
if (error)
|
|
25381
|
+
throw error;
|
|
25382
|
+
return response;
|
|
25383
|
+
}
|
|
25384
|
+
loggerDahua.warn(
|
|
25385
|
+
`[${host}] ${label}: ${why} on try ${attempt}/${DAHUA_COMMAND_ATTEMPTS}; trying again in ${nextDelayMs} ms`
|
|
25386
|
+
);
|
|
25387
|
+
await sleep(nextDelayMs);
|
|
25388
|
+
}
|
|
25389
|
+
}
|
|
25390
|
+
async function sendCameraCommandExpectingOk(options) {
|
|
25391
|
+
const response = await sendCameraCommand(options);
|
|
25392
|
+
const statusCode = getDahuaStatusCode(response);
|
|
25393
|
+
const text2 = getDahuaResponseText(response);
|
|
25394
|
+
if (statusCode === 200 && text2.includes("OK"))
|
|
25395
|
+
return response;
|
|
25396
|
+
const error = new Error(`Dahua response: ${text2 || `HTTP ${statusCode}`}`);
|
|
25397
|
+
error.answeredByCamera = true;
|
|
25398
|
+
throw error;
|
|
25399
|
+
}
|
|
25400
|
+
function anprOwnerName(owner) {
|
|
25401
|
+
return String(owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
|
|
25402
|
+
}
|
|
25403
|
+
function plateWindowOnCamera(value) {
|
|
25404
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 6e4);
|
|
25405
|
+
const tenYearsOn = new Date(tenMinutesAgo);
|
|
25406
|
+
tenYearsOn.setFullYear(tenYearsOn.getFullYear() + 10);
|
|
25407
|
+
return {
|
|
25408
|
+
start: dahuaWallClock(value.start, value.timezone) ?? dahuaWallClock(tenMinutesAgo, value.timezone),
|
|
25409
|
+
end: dahuaWallClock(value.end, value.timezone) ?? dahuaWallClock(tenYearsOn, value.timezone)
|
|
25410
|
+
};
|
|
25411
|
+
}
|
|
25009
25412
|
function createFileNameFromEvent(utcSeconds, utcMilliseconds) {
|
|
25010
25413
|
const date = new Date(utcSeconds * 1e3 + utcMilliseconds);
|
|
25011
25414
|
const year = date.getUTCFullYear();
|
|
@@ -25607,111 +26010,115 @@ function useDahuaService() {
|
|
|
25607
26010
|
start: import_joi26.default.string().isoDate().optional().allow("", null),
|
|
25608
26011
|
end: import_joi26.default.string().isoDate().optional().allow("", null),
|
|
25609
26012
|
owner: import_joi26.default.string().optional().allow("", null),
|
|
25610
|
-
isOpenGate: import_joi26.default.boolean().optional().allow(null)
|
|
26013
|
+
isOpenGate: import_joi26.default.boolean().optional().allow(null),
|
|
26014
|
+
timezone: import_joi26.default.string().optional().allow("", null)
|
|
25611
26015
|
});
|
|
25612
26016
|
const { error: validationError } = validation.validate(value);
|
|
25613
26017
|
if (validationError) {
|
|
25614
26018
|
throw new import_node_server_utils60.BadRequestError(`Validation error: ${validationError.message}`);
|
|
25615
26019
|
}
|
|
25616
26020
|
try {
|
|
25617
|
-
value.owner =
|
|
25618
|
-
const
|
|
25619
|
-
const date = dateStr ? new Date(dateStr) : /* @__PURE__ */ new Date();
|
|
25620
|
-
if (!dateStr) {
|
|
25621
|
-
date.setMinutes(date.getMinutes() - 10);
|
|
25622
|
-
}
|
|
25623
|
-
if (fallbackYearsAhead > 0 && !dateStr) {
|
|
25624
|
-
date.setFullYear(date.getFullYear() + fallbackYearsAhead);
|
|
25625
|
-
}
|
|
25626
|
-
const pad = (num) => String(num).padStart(2, "0");
|
|
25627
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
25628
|
-
};
|
|
25629
|
-
const formattedStart = formatDahuaDate3(value.start);
|
|
25630
|
-
const formattedEnd = formatDahuaDate3(value.end, 10);
|
|
26021
|
+
value.owner = anprOwnerName(value.owner);
|
|
26022
|
+
const { start: formattedStart, end: formattedEnd } = plateWindowOnCamera(value);
|
|
25631
26023
|
const beginTime = encodeURIComponent(formattedStart);
|
|
25632
26024
|
const cancelTime = encodeURIComponent(formattedEnd);
|
|
25633
26025
|
const plateNumber = encodeURIComponent(value.plateNumber);
|
|
25634
26026
|
const ownerName = encodeURIComponent(value.owner);
|
|
25635
26027
|
const endpoint = `/cgi-bin/recordUpdater.cgi?action=insert&name=${value.mode}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
|
|
25636
|
-
|
|
25637
|
-
|
|
25638
|
-
|
|
25639
|
-
|
|
25640
|
-
|
|
25641
|
-
|
|
25642
|
-
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25647
|
-
|
|
25648
|
-
|
|
25649
|
-
statusCode: 200,
|
|
25650
|
-
data: Buffer.from(`recno=${insertedId}`)
|
|
25651
|
-
};
|
|
25652
|
-
} catch (insertError) {
|
|
25653
|
-
const errorMessage = insertError?.message || String(insertError);
|
|
25654
|
-
if (/recno=/i.test(errorMessage)) {
|
|
25655
|
-
loggerDahua.info(`[${value.host}] Insert confirmed successfully via raw RecNo response text.`);
|
|
25656
|
-
const insertedId = errorMessage.match(/\d+/)?.[0] || "unknown";
|
|
26028
|
+
const INSERT_ROUNDS = 3;
|
|
26029
|
+
for (let round = 1; ; round++) {
|
|
26030
|
+
try {
|
|
26031
|
+
const insertResponse = await sendCameraCommandExpectingOk({
|
|
26032
|
+
host: value.host,
|
|
26033
|
+
username: value.username,
|
|
26034
|
+
password: value.password,
|
|
26035
|
+
endpoint,
|
|
26036
|
+
repeatable: false,
|
|
26037
|
+
label: `add plate to ${value.mode}`
|
|
26038
|
+
});
|
|
26039
|
+
const insertText = getDahuaResponseText(insertResponse);
|
|
26040
|
+
const insertedId = insertText.match(/\d+/)?.[0] || "unknown";
|
|
25657
26041
|
return {
|
|
26042
|
+
...insertResponse,
|
|
25658
26043
|
statusCode: 200,
|
|
25659
26044
|
data: Buffer.from(`recno=${insertedId}`)
|
|
25660
26045
|
};
|
|
25661
|
-
}
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
|
|
25667
|
-
|
|
25668
|
-
|
|
25669
|
-
|
|
25670
|
-
endpoint: findEndpoint,
|
|
25671
|
-
retries: 5
|
|
25672
|
-
});
|
|
25673
|
-
const textData = getDahuaResponseText(findResponse);
|
|
25674
|
-
recno = extractRecNo(textData);
|
|
25675
|
-
} catch (findError) {
|
|
25676
|
-
const findErrorMessage = findError?.message || String(findError);
|
|
25677
|
-
recno = extractRecNo(findErrorMessage);
|
|
25678
|
-
if (!recno) {
|
|
25679
|
-
loggerDahua.error(`[${value.host}] Failed handling existing duplicate plate flow`, findError);
|
|
25680
|
-
}
|
|
26046
|
+
} catch (insertError) {
|
|
26047
|
+
const errorMessage = insertError?.message || String(insertError);
|
|
26048
|
+
if (/recno=/i.test(errorMessage)) {
|
|
26049
|
+
loggerDahua.info(`[${value.host}] Insert confirmed successfully via raw RecNo response text.`);
|
|
26050
|
+
const insertedId = errorMessage.match(/\d+/)?.[0] || "unknown";
|
|
26051
|
+
return {
|
|
26052
|
+
statusCode: 200,
|
|
26053
|
+
data: Buffer.from(`recno=${insertedId}`)
|
|
26054
|
+
};
|
|
25681
26055
|
}
|
|
25682
|
-
|
|
25683
|
-
|
|
25684
|
-
|
|
26056
|
+
const duplicate = errorMessage.includes("Bad Request") || errorMessage.includes("Dahua response: Error");
|
|
26057
|
+
const lostInFlight = !insertError?.answeredByCamera && classifyDahuaAttempt({ error: insertError }) === "maybe-sent";
|
|
26058
|
+
if (duplicate || lostInFlight) {
|
|
26059
|
+
loggerDahua.info(
|
|
26060
|
+
`[${value.host}] Insert ${duplicate ? "failed (duplicate)" : "lost its connection"}. Checking if the plate is already on ${value.mode}...`
|
|
26061
|
+
);
|
|
26062
|
+
const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${plateNumber}`;
|
|
25685
26063
|
try {
|
|
25686
|
-
const
|
|
26064
|
+
const findResponse = await sendCameraCommand({
|
|
25687
26065
|
host: value.host,
|
|
25688
26066
|
username: value.username,
|
|
25689
26067
|
password: value.password,
|
|
25690
|
-
endpoint:
|
|
25691
|
-
|
|
26068
|
+
endpoint: findEndpoint,
|
|
26069
|
+
label: `find plate on ${value.mode}`
|
|
25692
26070
|
});
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
25699
|
-
|
|
25700
|
-
|
|
25701
|
-
|
|
25702
|
-
|
|
25703
|
-
|
|
25704
|
-
|
|
26071
|
+
if (getDahuaStatusCode(findResponse) === 200) {
|
|
26072
|
+
recno = extractRecNo(getDahuaResponseText(findResponse));
|
|
26073
|
+
} else {
|
|
26074
|
+
loggerDahua.error(
|
|
26075
|
+
`[${value.host}] Plate lookup after a failed insert answered HTTP ${getDahuaStatusCode(findResponse)}`
|
|
26076
|
+
);
|
|
26077
|
+
}
|
|
26078
|
+
} catch (findError) {
|
|
26079
|
+
loggerDahua.error(`[${value.host}] Failed handling existing duplicate plate flow`, findError);
|
|
26080
|
+
}
|
|
26081
|
+
if (recno) {
|
|
26082
|
+
loggerDahua.info(`[${value.host}] Found existing record ID: ${recno}. Updating registration instead...`);
|
|
26083
|
+
const updateEndpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${recno}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
|
|
26084
|
+
try {
|
|
26085
|
+
const updateResponse = await sendCameraCommandExpectingOk({
|
|
26086
|
+
host: value.host,
|
|
26087
|
+
username: value.username,
|
|
26088
|
+
password: value.password,
|
|
26089
|
+
endpoint: updateEndpoint,
|
|
26090
|
+
label: `update plate on ${value.mode}`
|
|
26091
|
+
});
|
|
25705
26092
|
return {
|
|
26093
|
+
...updateResponse,
|
|
25706
26094
|
statusCode: 200,
|
|
25707
26095
|
data: Buffer.from(`recno=${recno}`)
|
|
25708
26096
|
};
|
|
26097
|
+
} catch (updateError) {
|
|
26098
|
+
const updateErrorMessage = updateError?.message || String(updateError);
|
|
26099
|
+
if (/recno=/i.test(updateErrorMessage) || /ok/i.test(updateErrorMessage)) {
|
|
26100
|
+
loggerDahua.info(`[${value.host}] Update confirmed successfully despite wrapper omission.`);
|
|
26101
|
+
if (!recno) {
|
|
26102
|
+
recno = extractRecNo(updateErrorMessage);
|
|
26103
|
+
}
|
|
26104
|
+
return {
|
|
26105
|
+
statusCode: 200,
|
|
26106
|
+
data: Buffer.from(`recno=${recno}`)
|
|
26107
|
+
};
|
|
26108
|
+
}
|
|
26109
|
+
throw updateError;
|
|
25709
26110
|
}
|
|
25710
|
-
|
|
26111
|
+
}
|
|
26112
|
+
if (lostInFlight && round < INSERT_ROUNDS) {
|
|
26113
|
+
loggerDahua.warn(
|
|
26114
|
+
`[${value.host}] Plate not found after a lost insert; sending the insert again (round ${round + 1}/${INSERT_ROUNDS})`
|
|
26115
|
+
);
|
|
26116
|
+
await sleep(dahuaCommandRetryDelayMs(round));
|
|
26117
|
+
continue;
|
|
25711
26118
|
}
|
|
25712
26119
|
}
|
|
26120
|
+
throw insertError;
|
|
25713
26121
|
}
|
|
25714
|
-
throw insertError;
|
|
25715
26122
|
}
|
|
25716
26123
|
} catch (finalError) {
|
|
25717
26124
|
loggerDahua.error(
|
|
@@ -25731,7 +26138,8 @@ function useDahuaService() {
|
|
|
25731
26138
|
start: import_joi26.default.string().isoDate().optional().allow("", null),
|
|
25732
26139
|
end: import_joi26.default.string().isoDate().optional().allow("", null),
|
|
25733
26140
|
owner: import_joi26.default.string().optional().allow("", null),
|
|
25734
|
-
isOpenGate: import_joi26.default.boolean().optional()
|
|
26141
|
+
isOpenGate: import_joi26.default.boolean().optional(),
|
|
26142
|
+
timezone: import_joi26.default.string().optional().allow("", null)
|
|
25735
26143
|
});
|
|
25736
26144
|
const { error } = validation.validate(value);
|
|
25737
26145
|
if (error) {
|
|
@@ -25747,17 +26155,20 @@ function useDahuaService() {
|
|
|
25747
26155
|
mode: value.mode,
|
|
25748
26156
|
recno: value.recno,
|
|
25749
26157
|
plateNumber: value.plateNumber,
|
|
25750
|
-
|
|
25751
|
-
|
|
26158
|
+
// On the camera's clock (see `dahuaWallClock`); a time already written
|
|
26159
|
+
// for the camera passes through unchanged.
|
|
26160
|
+
start: dahuaWallClock(value.start, value.timezone) ?? value.start,
|
|
26161
|
+
end: dahuaWallClock(value.end, value.timezone) ?? value.end,
|
|
25752
26162
|
owner: value.owner,
|
|
25753
26163
|
openGate: _isOpenGate
|
|
25754
26164
|
});
|
|
25755
26165
|
try {
|
|
25756
|
-
const response = await
|
|
26166
|
+
const response = await sendCameraCommand({
|
|
25757
26167
|
host: value.host,
|
|
25758
26168
|
username: value.username,
|
|
25759
26169
|
password: value.password,
|
|
25760
|
-
endpoint
|
|
26170
|
+
endpoint,
|
|
26171
|
+
label: `update plate on ${value.mode}`
|
|
25761
26172
|
});
|
|
25762
26173
|
return response;
|
|
25763
26174
|
} catch (error2) {
|
|
@@ -25780,14 +26191,16 @@ function useDahuaService() {
|
|
|
25780
26191
|
throw new import_node_server_utils60.BadRequestError(`Validation error: ${error.message}`);
|
|
25781
26192
|
}
|
|
25782
26193
|
try {
|
|
25783
|
-
const response = await
|
|
26194
|
+
const response = await sendCameraCommand({
|
|
25784
26195
|
host: value.host,
|
|
25785
26196
|
username: value.username,
|
|
25786
26197
|
password: value.password,
|
|
25787
26198
|
endpoint: buildPlateRemoveEndpoint({
|
|
25788
26199
|
recno: value.recno,
|
|
25789
26200
|
mode: value.mode
|
|
25790
|
-
})
|
|
26201
|
+
}),
|
|
26202
|
+
repeatable: false,
|
|
26203
|
+
label: `remove plate from ${value.mode}`
|
|
25791
26204
|
});
|
|
25792
26205
|
return anprRevokeOutcome(response);
|
|
25793
26206
|
} catch (error2) {
|
|
@@ -25809,11 +26222,18 @@ function useDahuaService() {
|
|
|
25809
26222
|
throw new import_node_server_utils60.BadRequestError(`Validation error: ${error.message}`);
|
|
25810
26223
|
}
|
|
25811
26224
|
try {
|
|
25812
|
-
const
|
|
26225
|
+
const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${encodeURIComponent(value.plateNumber)}`;
|
|
26226
|
+
const response = value.requireOk ? await sendCameraCommand({
|
|
26227
|
+
host: value.host,
|
|
26228
|
+
username: value.username,
|
|
26229
|
+
password: value.password,
|
|
26230
|
+
endpoint: findEndpoint,
|
|
26231
|
+
label: `find plate on ${value.mode}`
|
|
26232
|
+
}) : await useDahuaDigest({
|
|
25813
26233
|
host: value.host,
|
|
25814
26234
|
username: value.username,
|
|
25815
26235
|
password: value.password,
|
|
25816
|
-
endpoint:
|
|
26236
|
+
endpoint: findEndpoint
|
|
25817
26237
|
});
|
|
25818
26238
|
if (value.requireOk) {
|
|
25819
26239
|
const statusCode = getDahuaStatusCode(response);
|
|
@@ -25840,7 +26260,8 @@ function useDahuaService() {
|
|
|
25840
26260
|
owner: import_joi26.default.string().optional().allow("", null),
|
|
25841
26261
|
isOpenGate: import_joi26.default.boolean().optional().allow(null),
|
|
25842
26262
|
vehicleType: import_joi26.default.string().optional().allow("", null),
|
|
25843
|
-
vehicleColor: import_joi26.default.string().optional().allow("", null)
|
|
26263
|
+
vehicleColor: import_joi26.default.string().optional().allow("", null),
|
|
26264
|
+
timezone: import_joi26.default.string().optional().allow("", null)
|
|
25844
26265
|
});
|
|
25845
26266
|
const { error } = validation.validate(value);
|
|
25846
26267
|
if (error) {
|
|
@@ -25856,17 +26277,20 @@ function useDahuaService() {
|
|
|
25856
26277
|
plateNumber: value.plateNumber,
|
|
25857
26278
|
vehicleType: value.vehicleType,
|
|
25858
26279
|
vehicleColor: value.vehicleColor,
|
|
25859
|
-
|
|
25860
|
-
|
|
26280
|
+
// On the camera's clock (see `dahuaWallClock`).
|
|
26281
|
+
start: dahuaWallClock(value.start, value.timezone) ?? value.start,
|
|
26282
|
+
end: dahuaWallClock(value.end, value.timezone) ?? value.end,
|
|
25861
26283
|
owner: value.owner,
|
|
25862
26284
|
openGate: isOpenGateString
|
|
25863
26285
|
});
|
|
25864
26286
|
try {
|
|
25865
|
-
const response = await
|
|
26287
|
+
const response = await sendCameraCommand({
|
|
25866
26288
|
host: value.host,
|
|
25867
26289
|
username: value.username,
|
|
25868
26290
|
password: value.password,
|
|
25869
|
-
endpoint
|
|
26291
|
+
endpoint,
|
|
26292
|
+
repeatable: false,
|
|
26293
|
+
label: `bulk add plate to ${value.mode}`
|
|
25870
26294
|
});
|
|
25871
26295
|
return response;
|
|
25872
26296
|
} catch (error2) {
|
|
@@ -25920,11 +26344,91 @@ function useDahuaService() {
|
|
|
25920
26344
|
throw error2;
|
|
25921
26345
|
}
|
|
25922
26346
|
}
|
|
26347
|
+
async function setPlateWindow(value) {
|
|
26348
|
+
const validation = import_joi26.default.object({
|
|
26349
|
+
host: import_joi26.default.string().required(),
|
|
26350
|
+
username: import_joi26.default.string().required(),
|
|
26351
|
+
password: import_joi26.default.string().required(),
|
|
26352
|
+
mode: import_joi26.default.string().valid(...Object.values(ANPRMode)).required(),
|
|
26353
|
+
recno: import_joi26.default.string().required(),
|
|
26354
|
+
plateNumber: import_joi26.default.string().required(),
|
|
26355
|
+
owner: import_joi26.default.string().optional().allow("", null),
|
|
26356
|
+
start: import_joi26.default.string().isoDate().optional().allow("", null),
|
|
26357
|
+
end: import_joi26.default.string().isoDate().optional().allow("", null),
|
|
26358
|
+
timezone: import_joi26.default.string().optional().allow("", null)
|
|
26359
|
+
});
|
|
26360
|
+
const { error } = validation.validate(value);
|
|
26361
|
+
if (error) {
|
|
26362
|
+
throw new import_node_server_utils60.BadRequestError(`Validation error: ${error.message}`);
|
|
26363
|
+
}
|
|
26364
|
+
const window = plateWindowOnCamera(value);
|
|
26365
|
+
const endpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${encodeURIComponent(value.mode)}&recno=${encodeURIComponent(value.recno)}&PlateNumber=${encodeURIComponent(value.plateNumber)}&BeginTime=${encodeURIComponent(window.start)}&CancelTime=${encodeURIComponent(window.end)}&MasterOfCar=${encodeURIComponent(anprOwnerName(value.owner))}`;
|
|
26366
|
+
try {
|
|
26367
|
+
return await sendCameraCommandExpectingOk({
|
|
26368
|
+
host: value.host,
|
|
26369
|
+
username: value.username,
|
|
26370
|
+
password: value.password,
|
|
26371
|
+
endpoint,
|
|
26372
|
+
label: `set plate window on ${value.mode}`
|
|
26373
|
+
});
|
|
26374
|
+
} catch (error2) {
|
|
26375
|
+
loggerDahua.error(
|
|
26376
|
+
`[${value.host}] Error setting plate window: ${error2?.message || error2}`
|
|
26377
|
+
);
|
|
26378
|
+
throw new import_node_server_utils60.BadRequestError(anprWriteReason(error2));
|
|
26379
|
+
}
|
|
26380
|
+
}
|
|
26381
|
+
async function clearPlateList(value) {
|
|
26382
|
+
const validation = import_joi26.default.object({
|
|
26383
|
+
host: import_joi26.default.string().required(),
|
|
26384
|
+
username: import_joi26.default.string().required(),
|
|
26385
|
+
password: import_joi26.default.string().required(),
|
|
26386
|
+
mode: import_joi26.default.string().valid(...Object.values(ANPRMode)).required()
|
|
26387
|
+
});
|
|
26388
|
+
const { error } = validation.validate(value);
|
|
26389
|
+
if (error) {
|
|
26390
|
+
return anprRevokeThrew(`Validation error: ${error.message}`);
|
|
26391
|
+
}
|
|
26392
|
+
loggerDahua.info(`clearPlateList called for host ${value.host} mode ${value.mode}`);
|
|
26393
|
+
try {
|
|
26394
|
+
const response = await sendCameraCommand({
|
|
26395
|
+
host: value.host,
|
|
26396
|
+
username: value.username,
|
|
26397
|
+
password: value.password,
|
|
26398
|
+
endpoint: buildPlateClearEndpoint({ mode: value.mode }),
|
|
26399
|
+
// A long list takes the device a while to empty.
|
|
26400
|
+
timeout: 6e4,
|
|
26401
|
+
deadlineMs: 9e4,
|
|
26402
|
+
label: `clear ${value.mode}`
|
|
26403
|
+
});
|
|
26404
|
+
const outcome = anprRevokeOutcome(response);
|
|
26405
|
+
if (!outcome.ok)
|
|
26406
|
+
return outcome;
|
|
26407
|
+
const text2 = getDahuaResponseText(response);
|
|
26408
|
+
if (!/\bOK\b/i.test(text2)) {
|
|
26409
|
+
loggerDahua.error(
|
|
26410
|
+
`[${value.host}] clearPlateList ${value.mode} answered 200 without OK: ${text2.slice(0, 200)}`
|
|
26411
|
+
);
|
|
26412
|
+
return {
|
|
26413
|
+
ok: false,
|
|
26414
|
+
statusCode: 200,
|
|
26415
|
+
reason: "the camera did not accept the command to clear its list",
|
|
26416
|
+
detail: `the camera answered 200 without OK: ${text2.slice(0, 200)}`
|
|
26417
|
+
};
|
|
26418
|
+
}
|
|
26419
|
+
return outcome;
|
|
26420
|
+
} catch (error2) {
|
|
26421
|
+
loggerDahua.error(`[${value.host}] Error clearing ${value.mode}:`, error2);
|
|
26422
|
+
return anprRevokeThrew(error2);
|
|
26423
|
+
}
|
|
26424
|
+
}
|
|
25923
26425
|
return {
|
|
25924
26426
|
getSnapshot,
|
|
25925
26427
|
listenToCamera,
|
|
25926
26428
|
addPlateNumber,
|
|
25927
26429
|
removePlateNumber,
|
|
26430
|
+
clearPlateList,
|
|
26431
|
+
setPlateWindow,
|
|
25928
26432
|
updatePlateNumber,
|
|
25929
26433
|
getPlateNumber,
|
|
25930
26434
|
bulkInsertPlateNumber,
|
|
@@ -26366,6 +26870,11 @@ function useSiteCameraRepo() {
|
|
|
26366
26870
|
{
|
|
26367
26871
|
$match: query2
|
|
26368
26872
|
},
|
|
26873
|
+
// A fixed order, so paging through a site's cameras 20 at a time
|
|
26874
|
+
// cannot skip one camera and return another twice.
|
|
26875
|
+
{
|
|
26876
|
+
$sort: { _id: 1 }
|
|
26877
|
+
},
|
|
26369
26878
|
{
|
|
26370
26879
|
$skip: value.page * value.limit
|
|
26371
26880
|
},
|
|
@@ -38085,6 +38594,7 @@ function useVehicleService() {
|
|
|
38085
38594
|
getVehicleByPlateNumber: _getVehicleByPlateNumber,
|
|
38086
38595
|
getAllExpiredVehicles: _getAllExpiredVehicles,
|
|
38087
38596
|
bulkUpsertVehicles: _bulkUpsertVehicles,
|
|
38597
|
+
getLivePlatesBySite: _getLivePlatesBySite,
|
|
38088
38598
|
findOne
|
|
38089
38599
|
} = useVehicleRepo();
|
|
38090
38600
|
const {
|
|
@@ -38095,12 +38605,441 @@ function useVehicleService() {
|
|
|
38095
38605
|
bulkInsertPlateNumber: _bulkInsertPlateNumber
|
|
38096
38606
|
} = useDahuaService();
|
|
38097
38607
|
const { getAllCameraWithPassword: _getAllSiteCameras } = useSiteCameraRepo();
|
|
38608
|
+
const { setPlateWindow: _setPlateWindow } = useDahuaService();
|
|
38098
38609
|
const { getById: _getById } = useOrgRepo();
|
|
38099
38610
|
const {
|
|
38100
38611
|
pushVehicleById: _pushVehicleById,
|
|
38101
38612
|
pullVehicleByRecNo: _pullVehicleByRecNo
|
|
38102
38613
|
} = usePersonRepo();
|
|
38103
38614
|
const { getUnitByBlockLevelUnitNumber: _getUnitByBlockLevelUnitNumber } = useBuildingUnitRepo();
|
|
38615
|
+
async function camerasAt(site, options = {}) {
|
|
38616
|
+
const siteId = String(site ?? "");
|
|
38617
|
+
if (!siteId)
|
|
38618
|
+
return [];
|
|
38619
|
+
const cameras = [];
|
|
38620
|
+
let page = 1;
|
|
38621
|
+
let pages = 1;
|
|
38622
|
+
const limit = 20;
|
|
38623
|
+
do {
|
|
38624
|
+
const siteCameraReq = await _getAllSiteCameras({
|
|
38625
|
+
site: siteId,
|
|
38626
|
+
type: "anpr",
|
|
38627
|
+
...options.directions ? { direction: options.directions } : {},
|
|
38628
|
+
...options.activeOnly ? { status: "active" } : {},
|
|
38629
|
+
page,
|
|
38630
|
+
limit
|
|
38631
|
+
});
|
|
38632
|
+
pages = siteCameraReq.pages || 1;
|
|
38633
|
+
cameras.push(...siteCameraReq.items);
|
|
38634
|
+
page++;
|
|
38635
|
+
} while (page <= pages);
|
|
38636
|
+
return cameras;
|
|
38637
|
+
}
|
|
38638
|
+
function allowCamerasAt(site) {
|
|
38639
|
+
return camerasAt(site, {
|
|
38640
|
+
directions: ALLOW_CAMERA_DIRECTIONS,
|
|
38641
|
+
activeOnly: true
|
|
38642
|
+
});
|
|
38643
|
+
}
|
|
38644
|
+
function blockCamerasAt(site) {
|
|
38645
|
+
return camerasAt(site, {
|
|
38646
|
+
directions: BLOCK_CAMERA_DIRECTIONS,
|
|
38647
|
+
activeOnly: true
|
|
38648
|
+
});
|
|
38649
|
+
}
|
|
38650
|
+
function listOf(mode) {
|
|
38651
|
+
return mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */ ? "blocked" : "allowed";
|
|
38652
|
+
}
|
|
38653
|
+
function listMode(type) {
|
|
38654
|
+
return type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
|
|
38655
|
+
}
|
|
38656
|
+
function validDate(value) {
|
|
38657
|
+
if (value === null || value === void 0 || value === "")
|
|
38658
|
+
return null;
|
|
38659
|
+
const date = new Date(value);
|
|
38660
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
38661
|
+
}
|
|
38662
|
+
function cameraWindow(start, end) {
|
|
38663
|
+
const startDate = validDate(start);
|
|
38664
|
+
const endDate = validDate(end);
|
|
38665
|
+
return {
|
|
38666
|
+
...startDate ? { start: startDate.toISOString() } : {},
|
|
38667
|
+
...endDate ? { end: endDate.toISOString() } : {}
|
|
38668
|
+
};
|
|
38669
|
+
}
|
|
38670
|
+
const siteZones = /* @__PURE__ */ new Map();
|
|
38671
|
+
async function siteTimezone(site) {
|
|
38672
|
+
const key = String(site ?? "");
|
|
38673
|
+
const cached = siteZones.get(key);
|
|
38674
|
+
if (cached && Date.now() - cached.at < 5 * 6e4)
|
|
38675
|
+
return cached.zone;
|
|
38676
|
+
let zone = resolveSiteTimezone(null);
|
|
38677
|
+
if (key) {
|
|
38678
|
+
try {
|
|
38679
|
+
const found = await useSiteRepo().getSiteById(key);
|
|
38680
|
+
zone = resolveSiteTimezone(found);
|
|
38681
|
+
} catch {
|
|
38682
|
+
zone = resolveSiteTimezone(null);
|
|
38683
|
+
}
|
|
38684
|
+
}
|
|
38685
|
+
siteZones.set(key, { zone, at: Date.now() });
|
|
38686
|
+
return zone;
|
|
38687
|
+
}
|
|
38688
|
+
function blockWindow(vehicleEnd) {
|
|
38689
|
+
const now = /* @__PURE__ */ new Date();
|
|
38690
|
+
let end = validDate(vehicleEnd);
|
|
38691
|
+
if (!end || end.getTime() <= now.getTime()) {
|
|
38692
|
+
end = new Date(now);
|
|
38693
|
+
end.setFullYear(end.getFullYear() + 10);
|
|
38694
|
+
}
|
|
38695
|
+
return { start: now.toISOString(), end: end.toISOString() };
|
|
38696
|
+
}
|
|
38697
|
+
async function findPlateOnCamera(camera, mode, plateNumber) {
|
|
38698
|
+
const text2 = await _getPlateNumber({
|
|
38699
|
+
host: camera.host,
|
|
38700
|
+
username: camera.username,
|
|
38701
|
+
password: camera.password,
|
|
38702
|
+
mode,
|
|
38703
|
+
plateNumber,
|
|
38704
|
+
requireOk: true
|
|
38705
|
+
});
|
|
38706
|
+
return matchingPlateRecords(text2 ?? "", plateNumber);
|
|
38707
|
+
}
|
|
38708
|
+
async function removePlateFromCamera(camera, mode, plateNumber) {
|
|
38709
|
+
if (!normalisePlate(plateNumber)) {
|
|
38710
|
+
return { ...ANPR_NOTHING_TO_REVOKE, changed: false };
|
|
38711
|
+
}
|
|
38712
|
+
let changed = false;
|
|
38713
|
+
const MAX_PASSES = 3;
|
|
38714
|
+
for (let pass = 0; pass <= MAX_PASSES; pass++) {
|
|
38715
|
+
let records;
|
|
38716
|
+
try {
|
|
38717
|
+
records = await findPlateOnCamera(camera, mode, plateNumber);
|
|
38718
|
+
} catch (error) {
|
|
38719
|
+
const outcome = anprRevokeThrew(error);
|
|
38720
|
+
logCameraFailure("plate lookup", camera, outcome.detail);
|
|
38721
|
+
return { ...outcome, changed };
|
|
38722
|
+
}
|
|
38723
|
+
if (!records.length)
|
|
38724
|
+
return { ok: true, reason: "", changed };
|
|
38725
|
+
if (pass === MAX_PASSES)
|
|
38726
|
+
break;
|
|
38727
|
+
for (const record of records) {
|
|
38728
|
+
const outcome = await _removePlateNumber({
|
|
38729
|
+
host: camera.host,
|
|
38730
|
+
username: camera.username,
|
|
38731
|
+
password: camera.password,
|
|
38732
|
+
recno: record.recNo,
|
|
38733
|
+
mode
|
|
38734
|
+
}).catch((error) => anprRevokeThrew(error));
|
|
38735
|
+
if (!outcome?.ok) {
|
|
38736
|
+
logCameraFailure(
|
|
38737
|
+
"plate remove",
|
|
38738
|
+
camera,
|
|
38739
|
+
outcome?.detail ?? "no outcome returned"
|
|
38740
|
+
);
|
|
38741
|
+
return {
|
|
38742
|
+
...outcome ?? anprRevokeThrew("no outcome returned"),
|
|
38743
|
+
changed
|
|
38744
|
+
};
|
|
38745
|
+
}
|
|
38746
|
+
changed = true;
|
|
38747
|
+
}
|
|
38748
|
+
}
|
|
38749
|
+
logCameraFailure(
|
|
38750
|
+
"plate remove",
|
|
38751
|
+
camera,
|
|
38752
|
+
`plate still listed after ${MAX_PASSES} rounds of removal`
|
|
38753
|
+
);
|
|
38754
|
+
return {
|
|
38755
|
+
ok: false,
|
|
38756
|
+
reason: "the camera did not remove the plate number",
|
|
38757
|
+
detail: `plate still listed after ${MAX_PASSES} rounds of removal`,
|
|
38758
|
+
changed
|
|
38759
|
+
};
|
|
38760
|
+
}
|
|
38761
|
+
async function insertPlateOnCamera(camera, write) {
|
|
38762
|
+
const response = await _addPlateNumber({
|
|
38763
|
+
host: camera.host,
|
|
38764
|
+
username: camera.username,
|
|
38765
|
+
password: camera.password,
|
|
38766
|
+
plateNumber: write.plateNumber,
|
|
38767
|
+
mode: write.mode,
|
|
38768
|
+
owner: write.owner,
|
|
38769
|
+
...write.start ? { start: write.start } : {},
|
|
38770
|
+
...write.end ? { end: write.end } : {},
|
|
38771
|
+
timezone: await siteTimezone(camera.site)
|
|
38772
|
+
});
|
|
38773
|
+
const status = response?.statusCode ?? response?.status ?? response?.res?.status;
|
|
38774
|
+
if (status !== 200)
|
|
38775
|
+
throw new Error(`HTTP ${status ?? "none"}`);
|
|
38776
|
+
return String(
|
|
38777
|
+
response?.data?.toString("utf-8")?.split("=")[1]?.trim() ?? ""
|
|
38778
|
+
);
|
|
38779
|
+
}
|
|
38780
|
+
async function ensurePlateOnCamera(camera, write) {
|
|
38781
|
+
const existing = await findPlateOnCamera(
|
|
38782
|
+
camera,
|
|
38783
|
+
write.mode,
|
|
38784
|
+
write.plateNumber
|
|
38785
|
+
);
|
|
38786
|
+
if (!existing.length) {
|
|
38787
|
+
await insertPlateOnCamera(camera, write);
|
|
38788
|
+
return { inserted: true };
|
|
38789
|
+
}
|
|
38790
|
+
const timezone = await siteTimezone(camera.site);
|
|
38791
|
+
const [keep, ...extra] = existing;
|
|
38792
|
+
const now = dahuaWallClock(/* @__PURE__ */ new Date(), timezone);
|
|
38793
|
+
const wanted = {
|
|
38794
|
+
start: dahuaWallClock(write.start, timezone),
|
|
38795
|
+
end: dahuaWallClock(write.end, timezone)
|
|
38796
|
+
};
|
|
38797
|
+
const windowIsRight = wanted.start && wanted.end ? plateWindowMatches(keep, wanted) : (
|
|
38798
|
+
// No dates of its own: any copy that lets it in now will do.
|
|
38799
|
+
Boolean(
|
|
38800
|
+
keep.beginTime && keep.cancelTime && now && (dahuaWallClock(keep.beginTime) ?? "") <= now && (dahuaWallClock(keep.cancelTime) ?? "") > now
|
|
38801
|
+
)
|
|
38802
|
+
);
|
|
38803
|
+
if (!windowIsRight) {
|
|
38804
|
+
await _setPlateWindow({
|
|
38805
|
+
host: camera.host,
|
|
38806
|
+
username: camera.username,
|
|
38807
|
+
password: camera.password,
|
|
38808
|
+
mode: write.mode,
|
|
38809
|
+
recno: keep.recNo,
|
|
38810
|
+
plateNumber: write.plateNumber,
|
|
38811
|
+
owner: write.owner,
|
|
38812
|
+
...write.start ? { start: write.start } : {},
|
|
38813
|
+
...write.end ? { end: write.end } : {},
|
|
38814
|
+
timezone
|
|
38815
|
+
});
|
|
38816
|
+
}
|
|
38817
|
+
for (const copy of extra) {
|
|
38818
|
+
const outcome = await _removePlateNumber({
|
|
38819
|
+
host: camera.host,
|
|
38820
|
+
username: camera.username,
|
|
38821
|
+
password: camera.password,
|
|
38822
|
+
recno: copy.recNo,
|
|
38823
|
+
mode: write.mode
|
|
38824
|
+
}).catch((error) => anprRevokeThrew(error));
|
|
38825
|
+
if (!outcome?.ok) {
|
|
38826
|
+
throw new Error(
|
|
38827
|
+
outcome?.reason || "the camera did not remove an old copy of the plate"
|
|
38828
|
+
);
|
|
38829
|
+
}
|
|
38830
|
+
}
|
|
38831
|
+
return { inserted: false };
|
|
38832
|
+
}
|
|
38833
|
+
function cameraJournal() {
|
|
38834
|
+
const steps = [];
|
|
38835
|
+
return {
|
|
38836
|
+
/** A plate was inserted; undo = take it off again. */
|
|
38837
|
+
inserted(camera, mode, plateNumber) {
|
|
38838
|
+
steps.push({
|
|
38839
|
+
camera,
|
|
38840
|
+
undo: () => removePlateFromCamera(camera, mode, plateNumber).then(
|
|
38841
|
+
(outcome) => outcome.ok,
|
|
38842
|
+
() => false
|
|
38843
|
+
)
|
|
38844
|
+
});
|
|
38845
|
+
},
|
|
38846
|
+
/** A plate was removed; undo = put it back as it was. */
|
|
38847
|
+
removed(camera, previous) {
|
|
38848
|
+
steps.push({
|
|
38849
|
+
camera,
|
|
38850
|
+
undo: () => insertPlateOnCamera(camera, previous).then(
|
|
38851
|
+
() => true,
|
|
38852
|
+
() => false
|
|
38853
|
+
)
|
|
38854
|
+
});
|
|
38855
|
+
},
|
|
38856
|
+
/**
|
|
38857
|
+
* Undo everything, newest first. Never throws. Returns the cameras that
|
|
38858
|
+
* could not be put back, so the operator can be told which barrier is
|
|
38859
|
+
* now wrong.
|
|
38860
|
+
*/
|
|
38861
|
+
async rollback() {
|
|
38862
|
+
const stranded = [];
|
|
38863
|
+
for (const step of steps.reverse()) {
|
|
38864
|
+
if (!await step.undo())
|
|
38865
|
+
stranded.push(cameraLabel(step.camera));
|
|
38866
|
+
}
|
|
38867
|
+
steps.length = 0;
|
|
38868
|
+
return Array.from(new Set(stranded));
|
|
38869
|
+
}
|
|
38870
|
+
};
|
|
38871
|
+
}
|
|
38872
|
+
function strandedWarning(stranded) {
|
|
38873
|
+
if (!stranded.length)
|
|
38874
|
+
return "";
|
|
38875
|
+
return ` WARNING: ${stranded.join(", ")} could not be put back the way ${stranded.length === 1 ? "it was" : "they were"} - check this plate on ${stranded.length === 1 ? "that camera" : "those cameras"} by hand.`;
|
|
38876
|
+
}
|
|
38877
|
+
async function rollbackCameras(journal, error) {
|
|
38878
|
+
const stranded = await journal.rollback().catch(() => []);
|
|
38879
|
+
if (stranded.length) {
|
|
38880
|
+
import_node_server_utils106.logger.error(
|
|
38881
|
+
`vehicle camera rollback could not restore: ${stranded.join(", ")}`
|
|
38882
|
+
);
|
|
38883
|
+
if (error && typeof error === "object") {
|
|
38884
|
+
error.message = `${error.message ?? ""}${strandedWarning(stranded)}`;
|
|
38885
|
+
}
|
|
38886
|
+
}
|
|
38887
|
+
}
|
|
38888
|
+
async function revokableModes(site, plateNumber, exclude) {
|
|
38889
|
+
const others = [];
|
|
38890
|
+
let siteId = null;
|
|
38891
|
+
try {
|
|
38892
|
+
siteId = new import_mongodb72.ObjectId(String(site));
|
|
38893
|
+
} catch {
|
|
38894
|
+
siteId = null;
|
|
38895
|
+
}
|
|
38896
|
+
if (siteId && plateNumber) {
|
|
38897
|
+
const excludeIds = exclude.map((id) => {
|
|
38898
|
+
try {
|
|
38899
|
+
return new import_mongodb72.ObjectId(String(id));
|
|
38900
|
+
} catch {
|
|
38901
|
+
return null;
|
|
38902
|
+
}
|
|
38903
|
+
}).filter((id) => id !== null);
|
|
38904
|
+
const base = {
|
|
38905
|
+
site: siteId,
|
|
38906
|
+
plateNumber,
|
|
38907
|
+
status: "active" /* ACTIVE */,
|
|
38908
|
+
_id: { $nin: excludeIds }
|
|
38909
|
+
};
|
|
38910
|
+
const [allowHolder, blockHolder] = await Promise.all([
|
|
38911
|
+
findOne({
|
|
38912
|
+
...base,
|
|
38913
|
+
$or: [
|
|
38914
|
+
{ type: "whitelist" /* WHITELIST */ },
|
|
38915
|
+
{ blockedFrom: "whitelist" /* WHITELIST */ }
|
|
38916
|
+
]
|
|
38917
|
+
}),
|
|
38918
|
+
findOne({ ...base, type: "blocklist" /* BLOCKLIST */ })
|
|
38919
|
+
]);
|
|
38920
|
+
if (allowHolder)
|
|
38921
|
+
others.push(allowHolder);
|
|
38922
|
+
if (blockHolder)
|
|
38923
|
+
others.push(blockHolder);
|
|
38924
|
+
}
|
|
38925
|
+
const lists = listsToRevoke(others);
|
|
38926
|
+
return [
|
|
38927
|
+
...lists.allow ? ["TrafficRedList" /* TRAFFIC_REDLIST */] : [],
|
|
38928
|
+
...lists.block ? ["TrafficBlackList" /* TRAFFIC_BLACKLIST */] : []
|
|
38929
|
+
];
|
|
38930
|
+
}
|
|
38931
|
+
async function removeFromCameras(cameras, modes, plateNumber, previous, journal, vehicleIsBlocked = false) {
|
|
38932
|
+
const failures = [];
|
|
38933
|
+
for (const camera of cameras) {
|
|
38934
|
+
for (const mode of modes) {
|
|
38935
|
+
if (!mustRevokeFrom(camera.direction, listOf(mode), vehicleIsBlocked)) {
|
|
38936
|
+
continue;
|
|
38937
|
+
}
|
|
38938
|
+
const outcome = await removePlateFromCamera(camera, mode, plateNumber);
|
|
38939
|
+
if (outcome.changed) {
|
|
38940
|
+
journal.removed(camera, { ...previous, plateNumber, mode });
|
|
38941
|
+
}
|
|
38942
|
+
if (!outcome.ok) {
|
|
38943
|
+
failures.push({
|
|
38944
|
+
camera: cameraLabel(camera),
|
|
38945
|
+
reason: outcome.reason || "the camera could not be reached"
|
|
38946
|
+
});
|
|
38947
|
+
}
|
|
38948
|
+
}
|
|
38949
|
+
}
|
|
38950
|
+
return failures;
|
|
38951
|
+
}
|
|
38952
|
+
async function writeToCameras(cameras, write, journal, options = {}) {
|
|
38953
|
+
const failures = [];
|
|
38954
|
+
let recNo = "";
|
|
38955
|
+
for (const camera of cameras) {
|
|
38956
|
+
try {
|
|
38957
|
+
if (options.onlyIfMissing) {
|
|
38958
|
+
const { inserted } = await ensurePlateOnCamera(camera, write);
|
|
38959
|
+
if (inserted)
|
|
38960
|
+
journal.inserted(camera, write.mode, write.plateNumber);
|
|
38961
|
+
} else {
|
|
38962
|
+
recNo = await insertPlateOnCamera(camera, write) || recNo;
|
|
38963
|
+
journal.inserted(camera, write.mode, write.plateNumber);
|
|
38964
|
+
}
|
|
38965
|
+
} catch (error) {
|
|
38966
|
+
logCameraFailure(`write ${write.mode}`, camera, error);
|
|
38967
|
+
failures.push({
|
|
38968
|
+
camera: cameraLabel(camera),
|
|
38969
|
+
reason: anprWriteReason(error)
|
|
38970
|
+
});
|
|
38971
|
+
}
|
|
38972
|
+
}
|
|
38973
|
+
return { failures, recNo };
|
|
38974
|
+
}
|
|
38975
|
+
async function blockOnCameras(vehicle, site, owner, journal) {
|
|
38976
|
+
const plateNumber = String(vehicle.plateNumber ?? "");
|
|
38977
|
+
if (!plateNumber)
|
|
38978
|
+
return;
|
|
38979
|
+
const cameras = await blockCamerasAt(site);
|
|
38980
|
+
if (!cameras.length) {
|
|
38981
|
+
throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("blocked"));
|
|
38982
|
+
}
|
|
38983
|
+
const { failures } = await writeToCameras(
|
|
38984
|
+
cameras,
|
|
38985
|
+
{
|
|
38986
|
+
plateNumber,
|
|
38987
|
+
mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
|
|
38988
|
+
owner,
|
|
38989
|
+
...blockWindow(vehicle.end)
|
|
38990
|
+
},
|
|
38991
|
+
journal,
|
|
38992
|
+
{ onlyIfMissing: true }
|
|
38993
|
+
);
|
|
38994
|
+
if (failures.length) {
|
|
38995
|
+
throw new import_node_server_utils106.BadRequestError(anprBlockFailureMessage(failures));
|
|
38996
|
+
}
|
|
38997
|
+
}
|
|
38998
|
+
async function unblockOnCameras(vehicle, site, options, journal) {
|
|
38999
|
+
const plateNumber = String(vehicle.plateNumber ?? "");
|
|
39000
|
+
if (!plateNumber)
|
|
39001
|
+
return;
|
|
39002
|
+
const window = cameraWindow(vehicle.start, vehicle.end);
|
|
39003
|
+
if (options.restore) {
|
|
39004
|
+
const allowCameras = await allowCamerasAt(site);
|
|
39005
|
+
const { failures: failures2 } = await writeToCameras(
|
|
39006
|
+
allowCameras,
|
|
39007
|
+
{
|
|
39008
|
+
plateNumber,
|
|
39009
|
+
mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
39010
|
+
owner: options.owner,
|
|
39011
|
+
...window
|
|
39012
|
+
},
|
|
39013
|
+
journal,
|
|
39014
|
+
{ onlyIfMissing: true }
|
|
39015
|
+
);
|
|
39016
|
+
if (failures2.length) {
|
|
39017
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39018
|
+
cameraFailureSentence({
|
|
39019
|
+
failures: failures2,
|
|
39020
|
+
operation: "added to",
|
|
39021
|
+
outcome: "unblocked",
|
|
39022
|
+
consequence: "it is still blocked at the barrier"
|
|
39023
|
+
})
|
|
39024
|
+
);
|
|
39025
|
+
}
|
|
39026
|
+
}
|
|
39027
|
+
const revokable = await revokableModes(site, plateNumber, [vehicle._id]);
|
|
39028
|
+
const modes = options.restore ? revokable.filter((mode) => mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */) : revokable;
|
|
39029
|
+
if (!modes.length)
|
|
39030
|
+
return;
|
|
39031
|
+
const failures = await removeFromCameras(
|
|
39032
|
+
await camerasAt(site, { activeOnly: true }),
|
|
39033
|
+
modes,
|
|
39034
|
+
plateNumber,
|
|
39035
|
+
{ owner: options.owner, ...window },
|
|
39036
|
+
journal,
|
|
39037
|
+
true
|
|
39038
|
+
);
|
|
39039
|
+
if (failures.length) {
|
|
39040
|
+
throw new import_node_server_utils106.BadRequestError(anprUnblockFailureMessage(failures));
|
|
39041
|
+
}
|
|
39042
|
+
}
|
|
38104
39043
|
async function add(value, session, user) {
|
|
38105
39044
|
const isExternalSession = !!session;
|
|
38106
39045
|
if (!session) {
|
|
@@ -38155,18 +39094,18 @@ function useVehicleService() {
|
|
|
38155
39094
|
endDate.setFullYear(endDate.getFullYear() + 10);
|
|
38156
39095
|
start = startDate.toISOString();
|
|
38157
39096
|
end = endDate.toISOString();
|
|
38158
|
-
startDateDahua =
|
|
38159
|
-
endDateDahua =
|
|
39097
|
+
startDateDahua = start;
|
|
39098
|
+
endDateDahua = end;
|
|
38160
39099
|
} else {
|
|
38161
39100
|
if (hasStart) {
|
|
38162
39101
|
startDate = new Date(value.start);
|
|
38163
39102
|
start = startDate.toISOString();
|
|
38164
|
-
startDateDahua =
|
|
39103
|
+
startDateDahua = start;
|
|
38165
39104
|
}
|
|
38166
39105
|
if (hasEnd) {
|
|
38167
39106
|
endDate = new Date(value.end);
|
|
38168
39107
|
end = endDate.toISOString();
|
|
38169
|
-
endDateDahua =
|
|
39108
|
+
endDateDahua = end;
|
|
38170
39109
|
}
|
|
38171
39110
|
}
|
|
38172
39111
|
const owner = value.name;
|
|
@@ -38179,22 +39118,7 @@ function useVehicleService() {
|
|
|
38179
39118
|
let message = "Vehicle plate number needs approval from property management.";
|
|
38180
39119
|
let siteCameras = [];
|
|
38181
39120
|
if (value.status && value.status !== "pending" /* PENDING */) {
|
|
38182
|
-
|
|
38183
|
-
let pages = 1;
|
|
38184
|
-
const limit = 20;
|
|
38185
|
-
do {
|
|
38186
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
38187
|
-
site: value.site,
|
|
38188
|
-
type: "anpr",
|
|
38189
|
-
direction: ["both", "entry", "residents"],
|
|
38190
|
-
page,
|
|
38191
|
-
limit,
|
|
38192
|
-
status: "active"
|
|
38193
|
-
});
|
|
38194
|
-
pages = siteCameraReq.pages || 1;
|
|
38195
|
-
siteCameras.push(...siteCameraReq.items);
|
|
38196
|
-
page++;
|
|
38197
|
-
} while (page <= pages);
|
|
39121
|
+
siteCameras = isBlocklist ? await blockCamerasAt(value.site) : await allowCamerasAt(value.site);
|
|
38198
39122
|
import_node_server_utils106.logger.info(
|
|
38199
39123
|
`addVehicle resolved ${siteCameras.length} site camera(s) for site ${value?.site}`
|
|
38200
39124
|
);
|
|
@@ -38229,7 +39153,10 @@ function useVehicleService() {
|
|
|
38229
39153
|
mode: _mode,
|
|
38230
39154
|
owner,
|
|
38231
39155
|
...startDateDahua ? { start: startDateDahua } : {},
|
|
38232
|
-
...endDateDahua ? { end: endDateDahua } : {}
|
|
39156
|
+
...endDateDahua ? { end: endDateDahua } : {},
|
|
39157
|
+
// The camera enforces the season pass window on its own clock,
|
|
39158
|
+
// in the site's zone.
|
|
39159
|
+
timezone: await siteTimezone(camera.site ?? value.site)
|
|
38233
39160
|
};
|
|
38234
39161
|
try {
|
|
38235
39162
|
const dahuaResponse = await _addPlateNumber(dahuaPayload);
|
|
@@ -38249,8 +39176,7 @@ function useVehicleService() {
|
|
|
38249
39176
|
}
|
|
38250
39177
|
const recNo = String(responseData.split("=")[1]?.trim() ?? "");
|
|
38251
39178
|
vehicleValue.recNo = recNo;
|
|
38252
|
-
|
|
38253
|
-
written.push({ camera, recno: recNo, mode: _mode });
|
|
39179
|
+
written.push({ camera, recno: recNo, mode: _mode, plateNumber: plateNumber2 });
|
|
38254
39180
|
if (value.peopleId && recNo) {
|
|
38255
39181
|
await _pushVehicleById(
|
|
38256
39182
|
value.peopleId,
|
|
@@ -38280,13 +39206,18 @@ function useVehicleService() {
|
|
|
38280
39206
|
} catch (error) {
|
|
38281
39207
|
const stranded = [];
|
|
38282
39208
|
for (const entry of written) {
|
|
38283
|
-
const
|
|
39209
|
+
const usableId = entry.recno && /^\d+$/.test(entry.recno);
|
|
39210
|
+
const outcome = usableId ? await _removePlateNumber({
|
|
38284
39211
|
host: entry.camera.host,
|
|
38285
39212
|
username: entry.camera.username,
|
|
38286
39213
|
password: entry.camera.password,
|
|
38287
39214
|
recno: entry.recno,
|
|
38288
39215
|
mode: entry.mode
|
|
38289
|
-
}).catch((removeError) => anprRevokeThrew(removeError))
|
|
39216
|
+
}).catch((removeError) => anprRevokeThrew(removeError)) : await removePlateFromCamera(
|
|
39217
|
+
entry.camera,
|
|
39218
|
+
entry.mode,
|
|
39219
|
+
entry.plateNumber
|
|
39220
|
+
);
|
|
38290
39221
|
if (!outcome?.ok)
|
|
38291
39222
|
stranded.push(cameraLabel(entry.camera));
|
|
38292
39223
|
}
|
|
@@ -38312,80 +39243,28 @@ function useVehicleService() {
|
|
|
38312
39243
|
if (!session) {
|
|
38313
39244
|
throw new Error("Unable to start session for vehicle service.");
|
|
38314
39245
|
}
|
|
38315
|
-
const
|
|
39246
|
+
const journal = cameraJournal();
|
|
38316
39247
|
try {
|
|
38317
39248
|
session.startTransaction();
|
|
38318
|
-
const siteCameras = [];
|
|
38319
|
-
let page = 1;
|
|
38320
|
-
let pages = 1;
|
|
38321
|
-
const limit = 20;
|
|
38322
|
-
do {
|
|
38323
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
38324
|
-
site,
|
|
38325
|
-
type: "anpr",
|
|
38326
|
-
page,
|
|
38327
|
-
limit
|
|
38328
|
-
});
|
|
38329
|
-
pages = siteCameraReq.pages || 1;
|
|
38330
|
-
siteCameras.push(...siteCameraReq.items);
|
|
38331
|
-
page++;
|
|
38332
|
-
} while (page <= pages);
|
|
38333
|
-
import_node_server_utils106.logger.info(
|
|
38334
|
-
`deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${site}`
|
|
38335
|
-
);
|
|
38336
39249
|
const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
|
|
38337
39250
|
const plateNumber = String(vehicle?.plateNumber ?? "");
|
|
38338
|
-
const
|
|
38339
|
-
|
|
38340
|
-
|
|
38341
|
-
|
|
38342
|
-
|
|
38343
|
-
|
|
38344
|
-
|
|
38345
|
-
|
|
38346
|
-
|
|
38347
|
-
|
|
38348
|
-
|
|
38349
|
-
|
|
38350
|
-
|
|
38351
|
-
|
|
38352
|
-
|
|
38353
|
-
|
|
38354
|
-
|
|
38355
|
-
}) ?? ""
|
|
38356
|
-
);
|
|
38357
|
-
if (!found.exists)
|
|
38358
|
-
continue;
|
|
38359
|
-
if (found.recNo)
|
|
38360
|
-
recnoForCamera = found.recNo;
|
|
38361
|
-
} catch (error) {
|
|
38362
|
-
const outcome2 = anprRevokeThrew(error);
|
|
38363
|
-
logCameraFailure("deleteVehicle lookup", camera, outcome2.detail);
|
|
38364
|
-
failures.push({ camera: label, reason: outcome2.reason });
|
|
38365
|
-
continue;
|
|
38366
|
-
}
|
|
38367
|
-
}
|
|
38368
|
-
if (!recnoForCamera)
|
|
38369
|
-
continue;
|
|
38370
|
-
const outcome = await _removePlateNumber({
|
|
38371
|
-
host,
|
|
38372
|
-
username,
|
|
38373
|
-
password,
|
|
38374
|
-
recno: recnoForCamera,
|
|
38375
|
-
mode: _mode
|
|
38376
|
-
});
|
|
38377
|
-
if (!outcome?.ok) {
|
|
38378
|
-
logCameraFailure(
|
|
38379
|
-
"deleteVehicle revoke",
|
|
38380
|
-
camera,
|
|
38381
|
-
outcome?.detail ?? "no outcome returned"
|
|
38382
|
-
);
|
|
38383
|
-
failures.push({
|
|
38384
|
-
camera: label,
|
|
38385
|
-
reason: outcome?.reason || "the camera could not be reached"
|
|
38386
|
-
});
|
|
38387
|
-
}
|
|
38388
|
-
}
|
|
39251
|
+
const cameraSite = vehicle?.site ?? site;
|
|
39252
|
+
const siteCameras = await camerasAt(cameraSite, { activeOnly: true });
|
|
39253
|
+
import_node_server_utils106.logger.info(
|
|
39254
|
+
`deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${cameraSite}`
|
|
39255
|
+
);
|
|
39256
|
+
const modes = plateNumber ? await revokableModes(cameraSite, plateNumber, [_id]) : [];
|
|
39257
|
+
const failures = await removeFromCameras(
|
|
39258
|
+
siteCameras,
|
|
39259
|
+
modes,
|
|
39260
|
+
plateNumber,
|
|
39261
|
+
{
|
|
39262
|
+
owner: String(vehicle?.name ?? ""),
|
|
39263
|
+
...cameraWindow(vehicle?.start, vehicle?.end)
|
|
39264
|
+
},
|
|
39265
|
+
journal,
|
|
39266
|
+
vehicle?.type === "blocklist" /* BLOCKLIST */
|
|
39267
|
+
);
|
|
38389
39268
|
if (failures.length) {
|
|
38390
39269
|
const message = anprRevokeFailureMessage(failures);
|
|
38391
39270
|
if (!bypass)
|
|
@@ -38395,13 +39274,14 @@ function useVehicleService() {
|
|
|
38395
39274
|
await _deleteVehicle(_id, session);
|
|
38396
39275
|
await _pullVehicleByRecNo(
|
|
38397
39276
|
recno,
|
|
38398
|
-
{ peopleId: vehicle?.peopleId, site, plateNumber },
|
|
39277
|
+
{ peopleId: vehicle?.peopleId, site: cameraSite, plateNumber },
|
|
38399
39278
|
session
|
|
38400
39279
|
);
|
|
38401
39280
|
await session.commitTransaction();
|
|
38402
39281
|
return `Vehicle plate number deleted from ${type} record successfully.`;
|
|
38403
39282
|
} catch (error) {
|
|
38404
39283
|
import_node_server_utils106.logger.error("Error in vehicle service delete:", error);
|
|
39284
|
+
await rollbackCameras(journal, error);
|
|
38405
39285
|
await session.abortTransaction();
|
|
38406
39286
|
throw error;
|
|
38407
39287
|
} finally {
|
|
@@ -38416,13 +39296,14 @@ function useVehicleService() {
|
|
|
38416
39296
|
const org = await _getById(orgId);
|
|
38417
39297
|
if (!org)
|
|
38418
39298
|
throw new import_node_server_utils106.BadRequestError("Org not found");
|
|
38419
|
-
const vehicle = await
|
|
38420
|
-
const plate = vehicle.plates.find((p) => p._id.toString() === id);
|
|
38421
|
-
const _plateNumber = plate?.plateNumber;
|
|
39299
|
+
const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(id) });
|
|
38422
39300
|
if (!vehicle) {
|
|
38423
|
-
throw new import_node_server_utils106.
|
|
39301
|
+
throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
|
|
38424
39302
|
}
|
|
39303
|
+
const _plateNumber = String(vehicle.plateNumber ?? "");
|
|
38425
39304
|
const owner = vehicle.name;
|
|
39305
|
+
const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
|
|
39306
|
+
const mode = listMode(vehicle.type);
|
|
38426
39307
|
const hasStart = typeof vehicle.start === "string" ? vehicle.start.trim() !== "" : !!vehicle.start;
|
|
38427
39308
|
const hasEnd = typeof vehicle.end === "string" ? vehicle.end.trim() !== "" : !!vehicle.end;
|
|
38428
39309
|
let startDate = null;
|
|
@@ -38440,70 +39321,48 @@ function useVehicleService() {
|
|
|
38440
39321
|
endDate.setFullYear(endDate.getFullYear() + 10);
|
|
38441
39322
|
}
|
|
38442
39323
|
}
|
|
38443
|
-
const startDahua = startDate ?
|
|
38444
|
-
const endDahua = endDate ?
|
|
39324
|
+
const startDahua = startDate ? startDate.toISOString() : "";
|
|
39325
|
+
const endDahua = endDate ? endDate.toISOString() : "";
|
|
39326
|
+
const journal = cameraJournal();
|
|
38445
39327
|
try {
|
|
38446
39328
|
session.startTransaction();
|
|
38447
|
-
const
|
|
38448
|
-
|
|
38449
|
-
let pages = 1;
|
|
38450
|
-
const limit = 20;
|
|
38451
|
-
let value = {};
|
|
38452
|
-
do {
|
|
38453
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
38454
|
-
site: siteId,
|
|
38455
|
-
type: "anpr",
|
|
38456
|
-
direction: ["both", "entry", "residents"],
|
|
38457
|
-
page,
|
|
38458
|
-
limit
|
|
38459
|
-
});
|
|
38460
|
-
pages = siteCameraReq.pages || 1;
|
|
38461
|
-
siteCameras.push(...siteCameraReq.items);
|
|
38462
|
-
page++;
|
|
38463
|
-
} while (page <= pages);
|
|
39329
|
+
const cameraSite = vehicle.site ?? siteId;
|
|
39330
|
+
const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
|
|
38464
39331
|
if (!siteCameras.length) {
|
|
38465
39332
|
throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("approved"));
|
|
38466
39333
|
}
|
|
38467
|
-
|
|
38468
|
-
|
|
38469
|
-
|
|
38470
|
-
host,
|
|
38471
|
-
username,
|
|
38472
|
-
password,
|
|
39334
|
+
const { failures, recNo } = await writeToCameras(
|
|
39335
|
+
siteCameras,
|
|
39336
|
+
{
|
|
38473
39337
|
plateNumber: _plateNumber,
|
|
38474
|
-
mode
|
|
39338
|
+
mode,
|
|
39339
|
+
owner,
|
|
38475
39340
|
start: startDahua,
|
|
38476
|
-
end: endDahua
|
|
38477
|
-
|
|
38478
|
-
|
|
38479
|
-
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
38483
|
-
|
|
38484
|
-
|
|
38485
|
-
|
|
38486
|
-
|
|
38487
|
-
|
|
38488
|
-
|
|
38489
|
-
camera: cameraLabel(camera),
|
|
38490
|
-
reason: anprWriteReason(
|
|
38491
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
38492
|
-
)
|
|
38493
|
-
}
|
|
38494
|
-
])
|
|
38495
|
-
);
|
|
38496
|
-
}
|
|
38497
|
-
const responseData = dahuaResponse?.data.toString("utf-8");
|
|
38498
|
-
value.recNo = responseData.split("=")[1]?.trim();
|
|
39341
|
+
end: endDahua
|
|
39342
|
+
},
|
|
39343
|
+
journal
|
|
39344
|
+
);
|
|
39345
|
+
if (failures.length) {
|
|
39346
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39347
|
+
cameraFailureSentence({
|
|
39348
|
+
failures,
|
|
39349
|
+
operation: "added to",
|
|
39350
|
+
outcome: "approved",
|
|
39351
|
+
consequence: isBlocklist ? "it would still open the barrier" : "it would not open the barrier"
|
|
39352
|
+
})
|
|
39353
|
+
);
|
|
38499
39354
|
}
|
|
38500
|
-
value
|
|
38501
|
-
|
|
39355
|
+
const value = {
|
|
39356
|
+
status: "active" /* ACTIVE */,
|
|
39357
|
+
// Kept for older readers only - one camera's number, never replayed.
|
|
39358
|
+
...recNo ? { recNo } : {}
|
|
39359
|
+
};
|
|
39360
|
+
if (vehicle.peopleId && _plateNumber) {
|
|
38502
39361
|
await _pushVehicleById(
|
|
38503
39362
|
vehicle.peopleId,
|
|
38504
39363
|
{
|
|
38505
39364
|
plateNumber: _plateNumber,
|
|
38506
|
-
recNo
|
|
39365
|
+
recNo
|
|
38507
39366
|
},
|
|
38508
39367
|
session
|
|
38509
39368
|
);
|
|
@@ -38518,6 +39377,7 @@ function useVehicleService() {
|
|
|
38518
39377
|
return "Vehicle plate number approved and added successfully.";
|
|
38519
39378
|
} catch (error) {
|
|
38520
39379
|
import_node_server_utils106.logger.error("Error in vehicle service approving and adding:", error);
|
|
39380
|
+
await rollbackCameras(journal, error);
|
|
38521
39381
|
await session.abortTransaction();
|
|
38522
39382
|
throw error;
|
|
38523
39383
|
} finally {
|
|
@@ -38526,71 +39386,38 @@ function useVehicleService() {
|
|
|
38526
39386
|
}
|
|
38527
39387
|
async function processDeletingExpiredVehicles() {
|
|
38528
39388
|
const vehicles = await _getAllExpiredVehicles();
|
|
39389
|
+
const sweptIds = vehicles.map((vehicle) => vehicle?._id);
|
|
39390
|
+
const modesByVehicle = /* @__PURE__ */ new Map();
|
|
38529
39391
|
const { deletableIds, skipped } = await sweepExpiredVehicles(vehicles, {
|
|
38530
|
-
|
|
38531
|
-
|
|
38532
|
-
|
|
38533
|
-
|
|
38534
|
-
const limit = 20;
|
|
38535
|
-
do {
|
|
38536
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
38537
|
-
site,
|
|
38538
|
-
type: "anpr",
|
|
38539
|
-
page,
|
|
38540
|
-
limit
|
|
38541
|
-
});
|
|
38542
|
-
pages = siteCameraReq.pages || 1;
|
|
38543
|
-
cameras.push(...siteCameraReq.items);
|
|
38544
|
-
page++;
|
|
38545
|
-
} while (page <= pages);
|
|
38546
|
-
return cameras;
|
|
38547
|
-
},
|
|
39392
|
+
// Every ACTIVE ANPR camera at the site (an inactive one is skipped, as
|
|
39393
|
+
// by every add and update); which of them each list is taken off is
|
|
39394
|
+
// `mustRevokeFrom`.
|
|
39395
|
+
camerasForSite: (site) => camerasAt(site, { activeOnly: true }),
|
|
38548
39396
|
revoke: async (camera, vehicle) => {
|
|
38549
|
-
const host = camera.host;
|
|
38550
|
-
const username = camera.username;
|
|
38551
|
-
const password = camera.password;
|
|
38552
|
-
const mode = vehicle?.type !== "whitelist" /* WHITELIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
|
|
38553
39397
|
const plateNumber = String(vehicle?.plateNumber ?? "");
|
|
38554
|
-
|
|
38555
|
-
if (plateNumber) {
|
|
38556
|
-
try {
|
|
38557
|
-
const found = parseDahuaFind(
|
|
38558
|
-
await _getPlateNumber({
|
|
38559
|
-
host,
|
|
38560
|
-
username,
|
|
38561
|
-
password,
|
|
38562
|
-
mode,
|
|
38563
|
-
plateNumber,
|
|
38564
|
-
requireOk: true
|
|
38565
|
-
}) ?? ""
|
|
38566
|
-
);
|
|
38567
|
-
if (!found.exists)
|
|
38568
|
-
return ANPR_NOTHING_TO_REVOKE;
|
|
38569
|
-
if (found.recNo)
|
|
38570
|
-
recnoForCamera = found.recNo;
|
|
38571
|
-
} catch (error) {
|
|
38572
|
-
const outcome2 = anprRevokeThrew(error);
|
|
38573
|
-
logCameraFailure("expiredVehicle lookup", camera, outcome2.detail);
|
|
38574
|
-
return outcome2;
|
|
38575
|
-
}
|
|
38576
|
-
}
|
|
38577
|
-
if (!recnoForCamera)
|
|
39398
|
+
if (!plateNumber)
|
|
38578
39399
|
return ANPR_NOTHING_TO_REVOKE;
|
|
38579
|
-
const
|
|
38580
|
-
|
|
38581
|
-
|
|
38582
|
-
|
|
38583
|
-
|
|
38584
|
-
|
|
38585
|
-
|
|
38586
|
-
|
|
38587
|
-
|
|
38588
|
-
|
|
38589
|
-
|
|
38590
|
-
|
|
38591
|
-
|
|
39400
|
+
const key = String(vehicle?._id ?? plateNumber);
|
|
39401
|
+
let modes = modesByVehicle.get(key);
|
|
39402
|
+
if (!modes) {
|
|
39403
|
+
modes = revokableModes(vehicle?.site, plateNumber, sweptIds);
|
|
39404
|
+
modesByVehicle.set(key, modes);
|
|
39405
|
+
}
|
|
39406
|
+
let resolved;
|
|
39407
|
+
try {
|
|
39408
|
+
resolved = await modes;
|
|
39409
|
+
} catch (error) {
|
|
39410
|
+
return anprRevokeThrew(error);
|
|
39411
|
+
}
|
|
39412
|
+
for (const mode of resolved) {
|
|
39413
|
+
const blocked = vehicle?.type === "blocklist" /* BLOCKLIST */;
|
|
39414
|
+
if (!mustRevokeFrom(camera.direction, listOf(mode), blocked))
|
|
39415
|
+
continue;
|
|
39416
|
+
const outcome = await removePlateFromCamera(camera, mode, plateNumber);
|
|
39417
|
+
if (!outcome.ok)
|
|
39418
|
+
return outcome;
|
|
38592
39419
|
}
|
|
38593
|
-
return
|
|
39420
|
+
return ANPR_NOTHING_TO_REVOKE;
|
|
38594
39421
|
},
|
|
38595
39422
|
cameraLabel
|
|
38596
39423
|
});
|
|
@@ -38617,90 +39444,83 @@ function useVehicleService() {
|
|
|
38617
39444
|
"Only property management can reactivate vehicles."
|
|
38618
39445
|
);
|
|
38619
39446
|
}
|
|
38620
|
-
const vehicle = await
|
|
38621
|
-
const plate = vehicle.plates.find((p) => p._id.toString() === id);
|
|
38622
|
-
const _plateNumber = plate?.plateNumber;
|
|
39447
|
+
const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(id) });
|
|
38623
39448
|
if (!vehicle) {
|
|
38624
|
-
throw new import_node_server_utils106.
|
|
39449
|
+
throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
|
|
38625
39450
|
}
|
|
39451
|
+
const _plateNumber = String(vehicle.plateNumber ?? "");
|
|
38626
39452
|
const owner = vehicle.name;
|
|
38627
|
-
|
|
38628
|
-
|
|
38629
|
-
|
|
38630
|
-
const
|
|
38631
|
-
|
|
38632
|
-
|
|
38633
|
-
|
|
39453
|
+
const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
|
|
39454
|
+
const mode = listMode(vehicle.type);
|
|
39455
|
+
const ownStart = validDate(vehicle.start);
|
|
39456
|
+
const ownEnd = validDate(vehicle.end);
|
|
39457
|
+
let startDate;
|
|
39458
|
+
let endDate;
|
|
39459
|
+
if (ownEnd) {
|
|
39460
|
+
if (ownEnd.getTime() <= Date.now()) {
|
|
39461
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39462
|
+
`This vehicle's expiry date (${dahuaWallClock(
|
|
39463
|
+
ownEnd,
|
|
39464
|
+
await siteTimezone(vehicle.site ?? siteId)
|
|
39465
|
+
)}) has already passed, so it cannot be restored as it is. Add it again with new dates.`
|
|
39466
|
+
);
|
|
39467
|
+
}
|
|
39468
|
+
startDate = ownStart ?? /* @__PURE__ */ new Date();
|
|
39469
|
+
endDate = ownEnd;
|
|
39470
|
+
} else {
|
|
39471
|
+
startDate = ownStart ?? /* @__PURE__ */ new Date();
|
|
39472
|
+
endDate = new Date(startDate);
|
|
39473
|
+
endDate.setFullYear(endDate.getFullYear() + 10);
|
|
39474
|
+
}
|
|
39475
|
+
const startDahua = startDate.toISOString();
|
|
39476
|
+
const endDahua = endDate.toISOString();
|
|
39477
|
+
const journal = cameraJournal();
|
|
38634
39478
|
try {
|
|
38635
39479
|
session.startTransaction();
|
|
38636
|
-
|
|
38637
|
-
const siteCameras =
|
|
38638
|
-
let page = 1;
|
|
38639
|
-
let pages = 1;
|
|
38640
|
-
const limit = 20;
|
|
38641
|
-
do {
|
|
38642
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
38643
|
-
site: siteId,
|
|
38644
|
-
type: "anpr",
|
|
38645
|
-
direction: ["both", "entry"],
|
|
38646
|
-
page,
|
|
38647
|
-
limit
|
|
38648
|
-
});
|
|
38649
|
-
pages = siteCameraReq.pages || 1;
|
|
38650
|
-
siteCameras.push(...siteCameraReq.items);
|
|
38651
|
-
page++;
|
|
38652
|
-
} while (page <= pages);
|
|
39480
|
+
const cameraSite = vehicle.site ?? siteId;
|
|
39481
|
+
const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
|
|
38653
39482
|
if (!siteCameras.length) {
|
|
38654
39483
|
throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("restored"));
|
|
38655
39484
|
}
|
|
38656
|
-
|
|
38657
|
-
|
|
38658
|
-
|
|
38659
|
-
host,
|
|
38660
|
-
username,
|
|
38661
|
-
password,
|
|
39485
|
+
const { failures, recNo } = await writeToCameras(
|
|
39486
|
+
siteCameras,
|
|
39487
|
+
{
|
|
38662
39488
|
plateNumber: _plateNumber,
|
|
38663
|
-
mode
|
|
39489
|
+
mode,
|
|
39490
|
+
owner,
|
|
38664
39491
|
start: startDahua,
|
|
38665
|
-
end: endDahua
|
|
38666
|
-
|
|
38667
|
-
|
|
38668
|
-
|
|
38669
|
-
|
|
38670
|
-
|
|
38671
|
-
|
|
38672
|
-
|
|
38673
|
-
|
|
38674
|
-
|
|
38675
|
-
|
|
38676
|
-
|
|
38677
|
-
|
|
38678
|
-
camera: cameraLabel(camera),
|
|
38679
|
-
reason: anprWriteReason(
|
|
38680
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
38681
|
-
)
|
|
38682
|
-
}
|
|
38683
|
-
])
|
|
38684
|
-
);
|
|
38685
|
-
}
|
|
38686
|
-
const responseData = dahuaResponse?.data.toString("utf-8");
|
|
38687
|
-
value.recNo = responseData.split("=")[1]?.trim();
|
|
39492
|
+
end: endDahua
|
|
39493
|
+
},
|
|
39494
|
+
journal
|
|
39495
|
+
);
|
|
39496
|
+
if (failures.length) {
|
|
39497
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39498
|
+
isBlocklist ? cameraFailureSentence({
|
|
39499
|
+
failures,
|
|
39500
|
+
operation: "added to",
|
|
39501
|
+
outcome: "restored",
|
|
39502
|
+
consequence: "it would still open the barrier"
|
|
39503
|
+
}) : anprRestoreFailureMessage(failures)
|
|
39504
|
+
);
|
|
38688
39505
|
}
|
|
38689
|
-
|
|
38690
|
-
if (vehicle.peopleId && value.recNo) {
|
|
39506
|
+
if (vehicle.peopleId && _plateNumber) {
|
|
38691
39507
|
await _pushVehicleById(
|
|
38692
39508
|
vehicle.peopleId,
|
|
38693
39509
|
{
|
|
38694
39510
|
plateNumber: _plateNumber,
|
|
38695
|
-
recNo
|
|
39511
|
+
recNo
|
|
38696
39512
|
},
|
|
38697
39513
|
session
|
|
38698
39514
|
);
|
|
38699
39515
|
}
|
|
38700
39516
|
const formattedValue = {
|
|
38701
|
-
|
|
38702
|
-
|
|
38703
|
-
|
|
39517
|
+
status: "active" /* ACTIVE */,
|
|
39518
|
+
// Kept for older readers only - one camera's number, never replayed.
|
|
39519
|
+
...recNo ? { recNo } : {},
|
|
39520
|
+
// Dates, not ISO strings: the expiry sweep only matches a BSON Date
|
|
39521
|
+
// (see `expiredVehicleSweepFilter`), so a string here never expired.
|
|
39522
|
+
start: startDate,
|
|
39523
|
+
end: endDate,
|
|
38704
39524
|
deletedAt: ""
|
|
38705
39525
|
};
|
|
38706
39526
|
await _updateVehicleById(id, formattedValue, session);
|
|
@@ -38708,21 +39528,62 @@ function useVehicleService() {
|
|
|
38708
39528
|
return "Vehicle reactivated successfully.";
|
|
38709
39529
|
} catch (error) {
|
|
38710
39530
|
import_node_server_utils106.logger.error("Error in vehicle service reactivation:", error);
|
|
39531
|
+
await rollbackCameras(journal, error);
|
|
38711
39532
|
await session.abortTransaction();
|
|
38712
39533
|
throw error;
|
|
38713
39534
|
} finally {
|
|
38714
39535
|
session.endSession();
|
|
38715
39536
|
}
|
|
38716
39537
|
}
|
|
38717
|
-
async function
|
|
38718
|
-
|
|
38719
|
-
|
|
38720
|
-
|
|
38721
|
-
|
|
38722
|
-
|
|
38723
|
-
|
|
39538
|
+
async function rewriteOnCameras(vehicle, site, next, journal) {
|
|
39539
|
+
const oldPlate = String(vehicle.plateNumber ?? "");
|
|
39540
|
+
const held = listsHeldBy(vehicle);
|
|
39541
|
+
const targets = [];
|
|
39542
|
+
if (held.allow) {
|
|
39543
|
+
targets.push({
|
|
39544
|
+
mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
39545
|
+
cameras: await allowCamerasAt(site)
|
|
39546
|
+
});
|
|
39547
|
+
}
|
|
39548
|
+
if (held.block) {
|
|
39549
|
+
targets.push({
|
|
39550
|
+
mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
|
|
39551
|
+
cameras: await blockCamerasAt(site)
|
|
39552
|
+
});
|
|
39553
|
+
}
|
|
39554
|
+
if (!targets.some((target) => target.cameras.length)) {
|
|
39555
|
+
throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("updated"));
|
|
39556
|
+
}
|
|
39557
|
+
const removeModes = next.plateNumber !== oldPlate ? await revokableModes(site, oldPlate, [vehicle._id]) : targets.map((target) => target.mode);
|
|
39558
|
+
const removeFailures = await removeFromCameras(
|
|
39559
|
+
await camerasAt(site, { activeOnly: true }),
|
|
39560
|
+
removeModes,
|
|
39561
|
+
oldPlate,
|
|
39562
|
+
{
|
|
39563
|
+
owner: String(vehicle.name ?? ""),
|
|
39564
|
+
...cameraWindow(vehicle.start, vehicle.end)
|
|
39565
|
+
},
|
|
39566
|
+
journal,
|
|
39567
|
+
held.block
|
|
39568
|
+
);
|
|
39569
|
+
if (removeFailures.length) {
|
|
39570
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39571
|
+
anprRevokeFailureMessage(removeFailures, "updated")
|
|
39572
|
+
);
|
|
39573
|
+
}
|
|
39574
|
+
const writeFailures = [];
|
|
39575
|
+
for (const target of targets) {
|
|
39576
|
+
const { failures } = await writeToCameras(
|
|
39577
|
+
target.cameras,
|
|
39578
|
+
{ ...next, mode: target.mode },
|
|
39579
|
+
journal
|
|
39580
|
+
);
|
|
39581
|
+
writeFailures.push(...failures);
|
|
39582
|
+
}
|
|
39583
|
+
if (writeFailures.length) {
|
|
39584
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39585
|
+
anprWriteFailureMessage(writeFailures, "updated")
|
|
38724
39586
|
);
|
|
38725
|
-
return false;
|
|
38726
39587
|
}
|
|
38727
39588
|
}
|
|
38728
39589
|
async function updateVehicleById(_id, value) {
|
|
@@ -38730,241 +39591,75 @@ function useVehicleService() {
|
|
|
38730
39591
|
if (!session) {
|
|
38731
39592
|
throw new Error("Unable to start session for vehicle service.");
|
|
38732
39593
|
}
|
|
39594
|
+
const journal = cameraJournal();
|
|
38733
39595
|
try {
|
|
38734
39596
|
session.startTransaction();
|
|
38735
|
-
const vehicle = await
|
|
38736
|
-
|
|
38737
|
-
|
|
38738
|
-
|
|
38739
|
-
const _start = vehicle.start;
|
|
38740
|
-
const _end = vehicle.end;
|
|
38741
|
-
const _recNo = plate.recNo;
|
|
38742
|
-
const _type = value.type ? value.type : plate.type;
|
|
39597
|
+
const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
|
|
39598
|
+
if (!vehicle) {
|
|
39599
|
+
throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
|
|
39600
|
+
}
|
|
38743
39601
|
if (value.peopleId) {
|
|
38744
39602
|
value.peopleId = new import_mongodb72.ObjectId(value.peopleId);
|
|
38745
39603
|
}
|
|
38746
39604
|
const { name, plateNumber, start, end, recNo, type, unit, site, block, level, ...rest } = value;
|
|
38747
|
-
|
|
38748
|
-
|
|
38749
|
-
|
|
38750
|
-
|
|
38751
|
-
|
|
38752
|
-
|
|
38753
|
-
|
|
38754
|
-
|
|
38755
|
-
|
|
38756
|
-
|
|
38757
|
-
|
|
38758
|
-
|
|
38759
|
-
|
|
38760
|
-
|
|
38761
|
-
|
|
38762
|
-
|
|
38763
|
-
|
|
38764
|
-
|
|
38765
|
-
|
|
38766
|
-
|
|
38767
|
-
|
|
38768
|
-
throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("updated"));
|
|
39605
|
+
const oldPlate = String(vehicle.plateNumber ?? "");
|
|
39606
|
+
const oldName = String(vehicle.name ?? "");
|
|
39607
|
+
const oldType = String(vehicle.type || "whitelist" /* WHITELIST */);
|
|
39608
|
+
const newPlate = plateNumber ? String(plateNumber).trim() : oldPlate;
|
|
39609
|
+
const newName = name ? String(name) : oldName;
|
|
39610
|
+
const newType = type ? String(type) : oldType;
|
|
39611
|
+
const cameraSite = vehicle.site ?? site;
|
|
39612
|
+
const startDate = validDate(start);
|
|
39613
|
+
const endDate = validDate(end);
|
|
39614
|
+
const datesChanged = !!startDate && startDate.getTime() !== validDate(vehicle.start)?.getTime() || !!endDate && endDate.getTime() !== validDate(vehicle.end)?.getTime();
|
|
39615
|
+
const isLive = vehicle.status === "active" /* ACTIVE */;
|
|
39616
|
+
let typeFields = {};
|
|
39617
|
+
if (newType !== oldType) {
|
|
39618
|
+
if (newPlate !== oldPlate) {
|
|
39619
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39620
|
+
"Change the plate number and the vehicle type in two separate steps."
|
|
39621
|
+
);
|
|
39622
|
+
}
|
|
39623
|
+
if (newType === "blocklist" /* BLOCKLIST */) {
|
|
39624
|
+
if (isLive) {
|
|
39625
|
+
await blockOnCameras(vehicle, cameraSite, newName, journal);
|
|
38769
39626
|
}
|
|
38770
|
-
|
|
38771
|
-
|
|
38772
|
-
|
|
38773
|
-
|
|
38774
|
-
|
|
38775
|
-
|
|
38776
|
-
|
|
38777
|
-
|
|
38778
|
-
|
|
38779
|
-
|
|
38780
|
-
|
|
38781
|
-
|
|
38782
|
-
removePlateNumber
|
|
38783
|
-
);
|
|
38784
|
-
if (!responseForDeletion?.ok) {
|
|
38785
|
-
logCameraFailure(
|
|
38786
|
-
"updateVehicleById revoke",
|
|
38787
|
-
camera,
|
|
38788
|
-
responseForDeletion?.detail ?? "no outcome returned"
|
|
38789
|
-
);
|
|
38790
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
38791
|
-
anprRevokeFailureMessage(
|
|
38792
|
-
[
|
|
38793
|
-
{
|
|
38794
|
-
camera: cameraLabel(camera),
|
|
38795
|
-
reason: responseForDeletion?.reason || "the camera could not be reached"
|
|
38796
|
-
}
|
|
38797
|
-
],
|
|
38798
|
-
"updated"
|
|
38799
|
-
)
|
|
38800
|
-
);
|
|
38801
|
-
}
|
|
38802
|
-
const dahuaPayload = {
|
|
38803
|
-
host,
|
|
38804
|
-
username,
|
|
38805
|
-
password,
|
|
38806
|
-
plateNumber,
|
|
38807
|
-
mode: value.type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
38808
|
-
//whitelist or blocklist
|
|
38809
|
-
owner: name,
|
|
38810
|
-
...startDahua ? { start: startDahua } : {},
|
|
38811
|
-
...endDahua ? { end: endDahua } : {}
|
|
38812
|
-
};
|
|
38813
|
-
let dahuaResponse;
|
|
38814
|
-
try {
|
|
38815
|
-
dahuaResponse = await _addPlateNumber(dahuaPayload);
|
|
38816
|
-
} catch (error) {
|
|
38817
|
-
logCameraFailure("updateVehicleById write", camera, error);
|
|
38818
|
-
const restored = await restorePlateOnCamera({
|
|
38819
|
-
...removePlateNumber,
|
|
38820
|
-
plateNumber: _plateNumber,
|
|
38821
|
-
owner: _name,
|
|
38822
|
-
...startDahua ? { start: startDahua } : {},
|
|
38823
|
-
...endDahua ? { end: endDahua } : {}
|
|
38824
|
-
});
|
|
38825
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
38826
|
-
anprWriteFailureMessage(
|
|
38827
|
-
[{ camera: cameraLabel(camera), reason: anprWriteReason(error) }],
|
|
38828
|
-
"updated"
|
|
38829
|
-
) + (restored ? ` The previous plate number ${_plateNumber} is still registered, so nothing has changed.` : ` WARNING: ${_plateNumber} was taken off this camera and could not be put back - it will NOT open the barrier until someone restores it.`)
|
|
38830
|
-
);
|
|
38831
|
-
}
|
|
38832
|
-
if (dahuaResponse?.statusCode !== 200) {
|
|
38833
|
-
logCameraFailure(
|
|
38834
|
-
"updateVehicleById write",
|
|
38835
|
-
camera,
|
|
38836
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
38837
|
-
);
|
|
38838
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
38839
|
-
anprWriteFailureMessage(
|
|
38840
|
-
[
|
|
38841
|
-
{
|
|
38842
|
-
camera: cameraLabel(camera),
|
|
38843
|
-
reason: anprWriteReason(
|
|
38844
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
38845
|
-
)
|
|
38846
|
-
}
|
|
38847
|
-
],
|
|
38848
|
-
"updated"
|
|
38849
|
-
)
|
|
38850
|
-
);
|
|
38851
|
-
}
|
|
38852
|
-
const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
|
|
38853
|
-
value.recNo = responseData.split("=")[1]?.trim();
|
|
38854
|
-
if (value.peopleId && value.recNo) {
|
|
38855
|
-
await _pushVehicleById(
|
|
38856
|
-
value.peopleId,
|
|
38857
|
-
{
|
|
38858
|
-
plateNumber,
|
|
38859
|
-
recNo: value.recNo
|
|
38860
|
-
},
|
|
38861
|
-
session
|
|
38862
|
-
);
|
|
38863
|
-
}
|
|
38864
|
-
} else {
|
|
38865
|
-
const existing = parseDahuaFind(
|
|
38866
|
-
await _getPlateNumber({
|
|
38867
|
-
host,
|
|
38868
|
-
username,
|
|
38869
|
-
password,
|
|
38870
|
-
mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
38871
|
-
plateNumber,
|
|
38872
|
-
requireOk: true
|
|
38873
|
-
}) ?? ""
|
|
38874
|
-
);
|
|
38875
|
-
if (existing.exists) {
|
|
38876
|
-
const outcome = await _removePlateNumber({
|
|
38877
|
-
host,
|
|
38878
|
-
username,
|
|
38879
|
-
password,
|
|
38880
|
-
mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
38881
|
-
recno: existing.recNo || _recNo
|
|
38882
|
-
});
|
|
38883
|
-
if (!outcome?.ok) {
|
|
38884
|
-
logCameraFailure(
|
|
38885
|
-
"updateVehicleById revoke",
|
|
38886
|
-
camera,
|
|
38887
|
-
outcome?.detail ?? "no outcome returned"
|
|
38888
|
-
);
|
|
38889
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
38890
|
-
anprRevokeFailureMessage([
|
|
38891
|
-
{
|
|
38892
|
-
camera: cameraLabel(camera),
|
|
38893
|
-
reason: outcome?.reason || "the camera could not be reached"
|
|
38894
|
-
}
|
|
38895
|
-
], "updated")
|
|
38896
|
-
);
|
|
38897
|
-
}
|
|
38898
|
-
}
|
|
38899
|
-
const dahuaPayload = {
|
|
38900
|
-
host,
|
|
38901
|
-
username,
|
|
38902
|
-
password,
|
|
38903
|
-
plateNumber,
|
|
38904
|
-
// recno: _recNo,
|
|
38905
|
-
mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
38906
|
-
start: startDahua,
|
|
38907
|
-
end: endDahua,
|
|
38908
|
-
owner: name ? name : _name
|
|
38909
|
-
};
|
|
38910
|
-
let dahuaResponse;
|
|
38911
|
-
try {
|
|
38912
|
-
dahuaResponse = await _addPlateNumber(dahuaPayload);
|
|
38913
|
-
} catch (error) {
|
|
38914
|
-
logCameraFailure("updateVehicleById write", camera, error);
|
|
38915
|
-
const restored = existing.exists ? await restorePlateOnCamera({
|
|
38916
|
-
...dahuaPayload,
|
|
38917
|
-
owner: _name
|
|
38918
|
-
}) : true;
|
|
38919
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
38920
|
-
anprWriteFailureMessage(
|
|
38921
|
-
[{ camera: cameraLabel(camera), reason: anprWriteReason(error) }],
|
|
38922
|
-
"updated"
|
|
38923
|
-
) + (restored ? " The vehicle is unchanged and still registered." : ` WARNING: ${plateNumber} was taken off this camera and could not be put back - it will NOT open the barrier until someone restores it.`)
|
|
38924
|
-
);
|
|
38925
|
-
}
|
|
38926
|
-
if (dahuaResponse?.statusCode !== 200) {
|
|
38927
|
-
logCameraFailure(
|
|
38928
|
-
"updateVehicleById write",
|
|
38929
|
-
camera,
|
|
38930
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
38931
|
-
);
|
|
38932
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
38933
|
-
anprWriteFailureMessage(
|
|
38934
|
-
[
|
|
38935
|
-
{
|
|
38936
|
-
camera: cameraLabel(camera),
|
|
38937
|
-
reason: anprWriteReason(
|
|
38938
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
38939
|
-
)
|
|
38940
|
-
}
|
|
38941
|
-
],
|
|
38942
|
-
"updated"
|
|
38943
|
-
)
|
|
38944
|
-
);
|
|
38945
|
-
}
|
|
38946
|
-
const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
|
|
38947
|
-
value.recNo = responseData.split("=")[1]?.trim();
|
|
38948
|
-
if (value.peopleId && value.recNo) {
|
|
38949
|
-
await _pushVehicleById(
|
|
38950
|
-
value.peopleId,
|
|
38951
|
-
{
|
|
38952
|
-
plateNumber,
|
|
38953
|
-
recNo: value.recNo
|
|
38954
|
-
},
|
|
38955
|
-
session
|
|
38956
|
-
);
|
|
38957
|
-
}
|
|
38958
|
-
}
|
|
39627
|
+
typeFields = {
|
|
39628
|
+
type: "blocklist" /* BLOCKLIST */,
|
|
39629
|
+
blockedFrom: oldType === "whitelist" /* WHITELIST */ ? "whitelist" /* WHITELIST */ : String(vehicle.blockedFrom ?? "")
|
|
39630
|
+
};
|
|
39631
|
+
} else {
|
|
39632
|
+
if (isLive) {
|
|
39633
|
+
await unblockOnCameras(
|
|
39634
|
+
vehicle,
|
|
39635
|
+
cameraSite,
|
|
39636
|
+
{ restore: true, owner: newName },
|
|
39637
|
+
journal
|
|
39638
|
+
);
|
|
38959
39639
|
}
|
|
39640
|
+
typeFields = { type: "whitelist" /* WHITELIST */, blockedFrom: "" };
|
|
38960
39641
|
}
|
|
39642
|
+
} else if (isLive && (newPlate !== oldPlate || newName !== oldName || datesChanged)) {
|
|
39643
|
+
await rewriteOnCameras(
|
|
39644
|
+
vehicle,
|
|
39645
|
+
cameraSite,
|
|
39646
|
+
{
|
|
39647
|
+
plateNumber: newPlate,
|
|
39648
|
+
owner: newName,
|
|
39649
|
+
...cameraWindow(startDate ?? vehicle.start, endDate ?? vehicle.end)
|
|
39650
|
+
},
|
|
39651
|
+
journal
|
|
39652
|
+
);
|
|
38961
39653
|
}
|
|
38962
39654
|
const formattedValue = {
|
|
38963
39655
|
...rest,
|
|
38964
39656
|
...name && { name },
|
|
38965
|
-
...plateNumber && { plateNumber },
|
|
38966
|
-
|
|
38967
|
-
|
|
39657
|
+
...plateNumber && { plateNumber: newPlate },
|
|
39658
|
+
// Dates, not ISO strings: the expiry sweep only matches a BSON Date
|
|
39659
|
+
// (see `expiredVehicleSweepFilter`), so an end date edited to a string
|
|
39660
|
+
// never expired.
|
|
39661
|
+
...startDate && { start: startDate },
|
|
39662
|
+
...endDate && { end: endDate },
|
|
38968
39663
|
...block && {
|
|
38969
39664
|
block: typeof block === "string" ? new import_mongodb72.ObjectId(block) : block
|
|
38970
39665
|
},
|
|
@@ -38974,13 +39669,27 @@ function useVehicleService() {
|
|
|
38974
39669
|
...unit && {
|
|
38975
39670
|
unit: typeof unit === "string" ? new import_mongodb72.ObjectId(unit) : unit
|
|
38976
39671
|
},
|
|
38977
|
-
...
|
|
38978
|
-
...
|
|
39672
|
+
...type && { type },
|
|
39673
|
+
...typeFields
|
|
38979
39674
|
};
|
|
38980
39675
|
await _updateVehicleById(_id, formattedValue, session);
|
|
39676
|
+
const peopleId = value.peopleId || vehicle.peopleId;
|
|
39677
|
+
if (isLive && peopleId && newPlate !== oldPlate) {
|
|
39678
|
+
await _pushVehicleById(
|
|
39679
|
+
peopleId,
|
|
39680
|
+
{ plateNumber: newPlate, recNo: "" },
|
|
39681
|
+
session
|
|
39682
|
+
);
|
|
39683
|
+
await _pullVehicleByRecNo(
|
|
39684
|
+
"",
|
|
39685
|
+
{ peopleId, site: cameraSite, plateNumber: oldPlate },
|
|
39686
|
+
session
|
|
39687
|
+
);
|
|
39688
|
+
}
|
|
38981
39689
|
await session.commitTransaction();
|
|
38982
39690
|
} catch (error) {
|
|
38983
39691
|
import_node_server_utils106.logger.error("Error in vehicle service update:", error);
|
|
39692
|
+
await rollbackCameras(journal, error);
|
|
38984
39693
|
await session.abortTransaction();
|
|
38985
39694
|
throw error;
|
|
38986
39695
|
} finally {
|
|
@@ -39016,25 +39725,12 @@ function useVehicleService() {
|
|
|
39016
39725
|
if (sanitizedValues.length === 0) {
|
|
39017
39726
|
throw new Error("No valid vehicle plate numbers found.");
|
|
39018
39727
|
}
|
|
39019
|
-
|
|
39020
|
-
|
|
39021
|
-
|
|
39022
|
-
|
|
39023
|
-
|
|
39024
|
-
|
|
39025
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
39026
|
-
site,
|
|
39027
|
-
type: "anpr",
|
|
39028
|
-
direction: ["both", "entry"],
|
|
39029
|
-
page,
|
|
39030
|
-
limit
|
|
39031
|
-
});
|
|
39032
|
-
pages = siteCameraReq.pages || 1;
|
|
39033
|
-
siteCameras.push(...siteCameraReq.items);
|
|
39034
|
-
page++;
|
|
39035
|
-
} while (page <= pages);
|
|
39036
|
-
}
|
|
39037
|
-
if (!siteCameras.length) {
|
|
39728
|
+
const isBlockRow = (row) => row.type !== "whitelist" /* WHITELIST */;
|
|
39729
|
+
const allowCameras = site ? await allowCamerasAt(site) : [];
|
|
39730
|
+
const blockCameras = site ? await blockCamerasAt(site) : [];
|
|
39731
|
+
const hasAllowRows = sanitizedValues.some((row) => !isBlockRow(row));
|
|
39732
|
+
const hasBlockRows = sanitizedValues.some((row) => isBlockRow(row));
|
|
39733
|
+
if (hasAllowRows && !allowCameras.length || hasBlockRows && !blockCameras.length) {
|
|
39038
39734
|
throw new Error("No site cameras found.");
|
|
39039
39735
|
}
|
|
39040
39736
|
const written = /* @__PURE__ */ new Map();
|
|
@@ -39043,58 +39739,63 @@ function useVehicleService() {
|
|
|
39043
39739
|
site,
|
|
39044
39740
|
plateNumber: value.plateNumber
|
|
39045
39741
|
}));
|
|
39046
|
-
const
|
|
39047
|
-
|
|
39048
|
-
|
|
39049
|
-
|
|
39050
|
-
|
|
39051
|
-
|
|
39052
|
-
|
|
39053
|
-
|
|
39054
|
-
|
|
39055
|
-
|
|
39056
|
-
|
|
39057
|
-
|
|
39058
|
-
|
|
39059
|
-
|
|
39060
|
-
|
|
39061
|
-
|
|
39062
|
-
|
|
39063
|
-
|
|
39064
|
-
|
|
39065
|
-
|
|
39066
|
-
|
|
39742
|
+
const writeRow = async (camera, row) => {
|
|
39743
|
+
const rowId = String(row._id);
|
|
39744
|
+
const vehicleValue = sanitizedValues[Number(rowId)];
|
|
39745
|
+
const plateNumber = vehicleValue.plateNumber;
|
|
39746
|
+
const { host, username, password } = camera;
|
|
39747
|
+
const dahuaPayload = {
|
|
39748
|
+
host,
|
|
39749
|
+
username,
|
|
39750
|
+
password,
|
|
39751
|
+
plateNumber,
|
|
39752
|
+
mode: vehicleValue.type === "whitelist" /* WHITELIST */ ? "TrafficRedList" /* TRAFFIC_REDLIST */ : "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
|
|
39753
|
+
...vehicleValue.start ? { start: String(vehicleValue.start) } : {},
|
|
39754
|
+
...vehicleValue.end ? { end: String(vehicleValue.end) } : {},
|
|
39755
|
+
...vehicleValue.name ? { owner: vehicleValue.name } : {},
|
|
39756
|
+
...vehicleValue.vehicleModel ? { vehicleType: vehicleValue.vehicleModel } : {},
|
|
39757
|
+
...vehicleValue.vehicleColor ? { vehicleColor: vehicleValue.vehicleColor } : {},
|
|
39758
|
+
// Dates on the camera's clock, in the site's zone.
|
|
39759
|
+
timezone: await siteTimezone(camera.site ?? site)
|
|
39760
|
+
};
|
|
39761
|
+
try {
|
|
39762
|
+
const dahuaResponse = await _bulkInsertPlateNumber(dahuaPayload);
|
|
39763
|
+
const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
|
|
39764
|
+
const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
|
|
39765
|
+
if (responseStatus !== 200) {
|
|
39766
|
+
logCameraFailure(
|
|
39767
|
+
"bulk add",
|
|
39768
|
+
camera,
|
|
39769
|
+
`HTTP ${responseStatus}: ${responseData.slice(0, 500)}`
|
|
39770
|
+
);
|
|
39771
|
+
return {
|
|
39772
|
+
ok: false,
|
|
39773
|
+
reason: anprWriteReason(`HTTP ${responseStatus}`)
|
|
39067
39774
|
};
|
|
39068
|
-
try {
|
|
39069
|
-
const dahuaResponse = await _bulkInsertPlateNumber(dahuaPayload);
|
|
39070
|
-
const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
|
|
39071
|
-
const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
|
|
39072
|
-
if (responseStatus !== 200) {
|
|
39073
|
-
logCameraFailure(
|
|
39074
|
-
"bulk add",
|
|
39075
|
-
camera,
|
|
39076
|
-
`HTTP ${responseStatus}: ${responseData.slice(0, 500)}`
|
|
39077
|
-
);
|
|
39078
|
-
return {
|
|
39079
|
-
ok: false,
|
|
39080
|
-
reason: anprWriteReason(`HTTP ${responseStatus}`)
|
|
39081
|
-
};
|
|
39082
|
-
}
|
|
39083
|
-
const recNo = String(responseData.split("=")[1]?.trim() ?? "");
|
|
39084
|
-
if (recNo) {
|
|
39085
|
-
vehicleValue.recNo = vehicleValue.recNo || recNo;
|
|
39086
|
-
const entries = written.get(rowId) ?? [];
|
|
39087
|
-
entries.push({ camera, recno: recNo });
|
|
39088
|
-
written.set(rowId, entries);
|
|
39089
|
-
}
|
|
39090
|
-
return { ok: true, reason: "" };
|
|
39091
|
-
} catch (error) {
|
|
39092
|
-
logCameraFailure("bulk add", camera, error);
|
|
39093
|
-
return { ok: false, reason: anprWriteReason(error) };
|
|
39094
|
-
}
|
|
39095
39775
|
}
|
|
39776
|
+
const recNo = String(responseData.split("=")[1]?.trim() ?? "");
|
|
39777
|
+
if (recNo) {
|
|
39778
|
+
vehicleValue.recNo = vehicleValue.recNo || recNo;
|
|
39779
|
+
const entries = written.get(rowId) ?? [];
|
|
39780
|
+
entries.push({ camera, recno: recNo });
|
|
39781
|
+
written.set(rowId, entries);
|
|
39782
|
+
}
|
|
39783
|
+
return { ok: true, reason: "" };
|
|
39784
|
+
} catch (error) {
|
|
39785
|
+
logCameraFailure("bulk add", camera, error);
|
|
39786
|
+
return { ok: false, reason: anprWriteReason(error) };
|
|
39096
39787
|
}
|
|
39788
|
+
};
|
|
39789
|
+
const allowPass = await sweepExpiredVehicles(
|
|
39790
|
+
sweepRows.filter((row) => !isBlockRow(sanitizedValues[Number(row._id)])),
|
|
39791
|
+
{ camerasForSite: async () => allowCameras, cameraLabel, revoke: writeRow }
|
|
39792
|
+
);
|
|
39793
|
+
const blockPass = await sweepExpiredVehicles(
|
|
39794
|
+
sweepRows.filter((row) => isBlockRow(sanitizedValues[Number(row._id)])),
|
|
39795
|
+
{ camerasForSite: async () => blockCameras, cameraLabel, revoke: writeRow }
|
|
39097
39796
|
);
|
|
39797
|
+
const savableIds = [...allowPass.deletableIds, ...blockPass.deletableIds];
|
|
39798
|
+
const skipped = [...allowPass.skipped, ...blockPass.skipped];
|
|
39098
39799
|
const failedRows = [];
|
|
39099
39800
|
for (const entry of skipped) {
|
|
39100
39801
|
const stranded = [];
|
|
@@ -39139,123 +39840,26 @@ function useVehicleService() {
|
|
|
39139
39840
|
if (!session) {
|
|
39140
39841
|
throw new Error("Unable to start session for vehicle service.");
|
|
39141
39842
|
}
|
|
39843
|
+
const journal = cameraJournal();
|
|
39142
39844
|
try {
|
|
39143
39845
|
session.startTransaction();
|
|
39144
|
-
const vehicle = await
|
|
39145
|
-
|
|
39146
|
-
|
|
39147
|
-
const _plateNumber = plate?.plateNumber;
|
|
39148
|
-
const _start = vehicle.start;
|
|
39149
|
-
const _end = vehicle.end;
|
|
39150
|
-
const _recNo = plate.recNo;
|
|
39151
|
-
const _type = plate.type;
|
|
39152
|
-
if (value.peopleId) {
|
|
39153
|
-
value.peopleId = new import_mongodb72.ObjectId(value.peopleId);
|
|
39846
|
+
const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
|
|
39847
|
+
if (!vehicle) {
|
|
39848
|
+
throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
|
|
39154
39849
|
}
|
|
39155
|
-
const
|
|
39156
|
-
const
|
|
39157
|
-
|
|
39158
|
-
|
|
39159
|
-
if (_plateNumber) {
|
|
39160
|
-
const siteCameras = [];
|
|
39161
|
-
let page = 1;
|
|
39162
|
-
let pages = 1;
|
|
39163
|
-
const limit = 20;
|
|
39164
|
-
do {
|
|
39165
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
39166
|
-
site,
|
|
39167
|
-
type: "anpr",
|
|
39168
|
-
direction: ["both", "entry"],
|
|
39169
|
-
page,
|
|
39170
|
-
limit
|
|
39171
|
-
});
|
|
39172
|
-
pages = siteCameraReq.pages || 1;
|
|
39173
|
-
siteCameras.push(...siteCameraReq.items);
|
|
39174
|
-
page++;
|
|
39175
|
-
} while (page <= pages);
|
|
39176
|
-
if (!siteCameras.length) {
|
|
39177
|
-
throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("blocked"));
|
|
39178
|
-
}
|
|
39179
|
-
for (const camera of siteCameras) {
|
|
39180
|
-
const { host, username, password } = camera;
|
|
39181
|
-
const removePlateNumber = {
|
|
39182
|
-
host,
|
|
39183
|
-
username,
|
|
39184
|
-
password,
|
|
39185
|
-
mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
39186
|
-
recno: _recNo
|
|
39187
|
-
};
|
|
39188
|
-
const responseForDeletion = await _removePlateNumber(
|
|
39189
|
-
removePlateNumber
|
|
39190
|
-
);
|
|
39191
|
-
if (!responseForDeletion?.ok) {
|
|
39192
|
-
logCameraFailure(
|
|
39193
|
-
"blocklistVehicles revoke",
|
|
39194
|
-
camera,
|
|
39195
|
-
responseForDeletion?.detail ?? "no outcome returned"
|
|
39196
|
-
);
|
|
39197
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
39198
|
-
anprRevokeFailureMessage(
|
|
39199
|
-
[
|
|
39200
|
-
{
|
|
39201
|
-
camera: cameraLabel(camera),
|
|
39202
|
-
reason: responseForDeletion?.reason || "the camera could not be reached"
|
|
39203
|
-
}
|
|
39204
|
-
],
|
|
39205
|
-
"updated"
|
|
39206
|
-
)
|
|
39207
|
-
);
|
|
39208
|
-
}
|
|
39209
|
-
const dahuaPayload = {
|
|
39210
|
-
host,
|
|
39211
|
-
username,
|
|
39212
|
-
password,
|
|
39213
|
-
plateNumber: _plateNumber,
|
|
39214
|
-
mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
|
|
39215
|
-
owner: _name,
|
|
39216
|
-
...startDahua ? { start: startDahua } : {},
|
|
39217
|
-
...endDahua ? { end: endDahua } : {}
|
|
39218
|
-
};
|
|
39219
|
-
const dahuaResponse = await _addPlateNumber(dahuaPayload);
|
|
39220
|
-
if (dahuaResponse?.statusCode !== 200) {
|
|
39221
|
-
logCameraFailure(
|
|
39222
|
-
"blocklistVehicles write",
|
|
39223
|
-
camera,
|
|
39224
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
39225
|
-
);
|
|
39226
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
39227
|
-
anprBlockFailureMessage([
|
|
39228
|
-
{
|
|
39229
|
-
camera: cameraLabel(camera),
|
|
39230
|
-
reason: anprWriteReason(
|
|
39231
|
-
`HTTP ${dahuaResponse?.statusCode ?? "none"}`
|
|
39232
|
-
)
|
|
39233
|
-
}
|
|
39234
|
-
])
|
|
39235
|
-
);
|
|
39236
|
-
}
|
|
39237
|
-
const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
|
|
39238
|
-
value.recNo = responseData.split("=")[1]?.trim();
|
|
39239
|
-
const normalizedPlateNumber = _plateNumber;
|
|
39240
|
-
if (value.peopleId && value.recNo) {
|
|
39241
|
-
await _pushVehicleById(
|
|
39242
|
-
value.peopleId,
|
|
39243
|
-
{
|
|
39244
|
-
plateNumber: normalizedPlateNumber,
|
|
39245
|
-
recNo: value.recNo
|
|
39246
|
-
},
|
|
39247
|
-
session
|
|
39248
|
-
);
|
|
39249
|
-
}
|
|
39250
|
-
}
|
|
39850
|
+
const owner = String(value.name || vehicle.name || "");
|
|
39851
|
+
const cameraSite = vehicle.site ?? value.site;
|
|
39852
|
+
if (vehicle.status === "active" /* ACTIVE */) {
|
|
39853
|
+
await blockOnCameras(vehicle, cameraSite, owner, journal);
|
|
39251
39854
|
}
|
|
39252
39855
|
const formattedValue = {
|
|
39253
39856
|
type: "blocklist" /* BLOCKLIST */,
|
|
39254
|
-
|
|
39857
|
+
blockedFrom: vehicle.type === "whitelist" /* WHITELIST */ ? "whitelist" /* WHITELIST */ : String(vehicle.blockedFrom ?? "")
|
|
39255
39858
|
};
|
|
39256
39859
|
await _updateVehicleById(_id, formattedValue, session);
|
|
39257
39860
|
await session.commitTransaction();
|
|
39258
39861
|
} catch (error) {
|
|
39862
|
+
await rollbackCameras(journal, error);
|
|
39259
39863
|
await session.abortTransaction();
|
|
39260
39864
|
throw error;
|
|
39261
39865
|
} finally {
|
|
@@ -39267,85 +39871,222 @@ function useVehicleService() {
|
|
|
39267
39871
|
if (!session) {
|
|
39268
39872
|
throw new Error("Unable to start session for vehicle service.");
|
|
39269
39873
|
}
|
|
39874
|
+
const journal = cameraJournal();
|
|
39270
39875
|
try {
|
|
39271
39876
|
session.startTransaction();
|
|
39272
|
-
const vehicle = await
|
|
39273
|
-
|
|
39274
|
-
|
|
39275
|
-
|
|
39877
|
+
const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
|
|
39878
|
+
if (!vehicle) {
|
|
39879
|
+
throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
|
|
39880
|
+
}
|
|
39881
|
+
if (vehicle.type !== "blocklist" /* BLOCKLIST */) {
|
|
39882
|
+
throw new import_node_server_utils106.BadRequestError("This vehicle is not on the blocklist.");
|
|
39883
|
+
}
|
|
39884
|
+
const _plateNumber = String(vehicle.plateNumber ?? "");
|
|
39885
|
+
const cameraSite = vehicle.site ?? value.site;
|
|
39886
|
+
const restore = vehicle.blockedFrom === "whitelist" /* WHITELIST */;
|
|
39276
39887
|
if (value.peopleId) {
|
|
39277
39888
|
value.peopleId = new import_mongodb72.ObjectId(value.peopleId);
|
|
39278
39889
|
}
|
|
39279
|
-
|
|
39280
|
-
|
|
39281
|
-
|
|
39282
|
-
|
|
39283
|
-
|
|
39284
|
-
|
|
39285
|
-
do {
|
|
39286
|
-
const siteCameraReq = await _getAllSiteCameras({
|
|
39287
|
-
site,
|
|
39288
|
-
type: "anpr",
|
|
39289
|
-
direction: ["both", "entry"],
|
|
39290
|
-
page,
|
|
39291
|
-
limit
|
|
39292
|
-
});
|
|
39293
|
-
pages = siteCameraReq.pages || 1;
|
|
39294
|
-
siteCameras.push(...siteCameraReq.items);
|
|
39295
|
-
page++;
|
|
39296
|
-
} while (page <= pages);
|
|
39297
|
-
import_node_server_utils106.logger.info(
|
|
39298
|
-
`removeFromBlocklist resolved ${siteCameras.length} ANPR camera(s)`
|
|
39890
|
+
if (vehicle.status === "active" /* ACTIVE */) {
|
|
39891
|
+
await unblockOnCameras(
|
|
39892
|
+
vehicle,
|
|
39893
|
+
cameraSite,
|
|
39894
|
+
{ restore, owner: String(vehicle.name ?? "") },
|
|
39895
|
+
journal
|
|
39299
39896
|
);
|
|
39300
|
-
|
|
39301
|
-
|
|
39302
|
-
|
|
39303
|
-
|
|
39304
|
-
|
|
39305
|
-
|
|
39306
|
-
|
|
39307
|
-
|
|
39308
|
-
|
|
39309
|
-
const responseForDeletion = await _removePlateNumber(
|
|
39310
|
-
removePlateNumber
|
|
39311
|
-
);
|
|
39312
|
-
if (!responseForDeletion?.ok) {
|
|
39313
|
-
logCameraFailure(
|
|
39314
|
-
"removeFromBlocklist revoke",
|
|
39315
|
-
camera,
|
|
39316
|
-
responseForDeletion?.detail ?? "no outcome returned"
|
|
39317
|
-
);
|
|
39318
|
-
throw new import_node_server_utils106.BadRequestError(
|
|
39319
|
-
anprUnblockFailureMessage([
|
|
39320
|
-
{
|
|
39321
|
-
camera: cameraLabel(camera),
|
|
39322
|
-
reason: responseForDeletion?.reason || "the camera could not be reached"
|
|
39323
|
-
}
|
|
39324
|
-
])
|
|
39325
|
-
);
|
|
39326
|
-
}
|
|
39327
|
-
}
|
|
39897
|
+
}
|
|
39898
|
+
if (restore) {
|
|
39899
|
+
await _updateVehicleById(
|
|
39900
|
+
_id,
|
|
39901
|
+
{ type: "whitelist" /* WHITELIST */, blockedFrom: "" },
|
|
39902
|
+
session
|
|
39903
|
+
);
|
|
39904
|
+
await session.commitTransaction();
|
|
39905
|
+
return "Vehicle unblocked and restored to the whitelist.";
|
|
39328
39906
|
}
|
|
39329
39907
|
await _deleteVehicle(_id, session);
|
|
39330
|
-
|
|
39908
|
+
const peopleId = value.peopleId || vehicle.peopleId;
|
|
39909
|
+
if ((peopleId || cameraSite) && _plateNumber) {
|
|
39331
39910
|
await _pullVehicleByRecNo(
|
|
39332
|
-
|
|
39911
|
+
"",
|
|
39333
39912
|
{
|
|
39334
|
-
peopleId
|
|
39335
|
-
site,
|
|
39913
|
+
peopleId,
|
|
39914
|
+
site: cameraSite,
|
|
39336
39915
|
plateNumber: _plateNumber
|
|
39337
39916
|
},
|
|
39338
39917
|
session
|
|
39339
39918
|
);
|
|
39340
39919
|
}
|
|
39341
39920
|
await session.commitTransaction();
|
|
39921
|
+
return "Vehicle removed from the blocklist.";
|
|
39342
39922
|
} catch (error) {
|
|
39923
|
+
await rollbackCameras(journal, error);
|
|
39343
39924
|
await session.abortTransaction();
|
|
39344
39925
|
throw error;
|
|
39345
39926
|
} finally {
|
|
39346
39927
|
session.endSession();
|
|
39347
39928
|
}
|
|
39348
39929
|
}
|
|
39930
|
+
async function fillCameraLists(site, camera, lists) {
|
|
39931
|
+
let target;
|
|
39932
|
+
if (camera.id) {
|
|
39933
|
+
const stored = (await camerasAt(site)).find(
|
|
39934
|
+
(candidate) => String(candidate._id) === String(camera.id)
|
|
39935
|
+
);
|
|
39936
|
+
if (!stored)
|
|
39937
|
+
throw new import_node_server_utils106.NotFoundError("Camera not found.");
|
|
39938
|
+
target = {
|
|
39939
|
+
...stored,
|
|
39940
|
+
...camera.host ? { host: camera.host } : {},
|
|
39941
|
+
...camera.username ? { username: camera.username } : {},
|
|
39942
|
+
...camera.password ? { password: camera.password } : {},
|
|
39943
|
+
...camera.direction ? { direction: camera.direction } : {}
|
|
39944
|
+
};
|
|
39945
|
+
} else {
|
|
39946
|
+
target = {
|
|
39947
|
+
host: String(camera.host ?? ""),
|
|
39948
|
+
username: String(camera.username ?? ""),
|
|
39949
|
+
password: String(camera.password ?? ""),
|
|
39950
|
+
name: camera.name,
|
|
39951
|
+
direction: camera.direction,
|
|
39952
|
+
// Whose clock the season pass windows are written on.
|
|
39953
|
+
site
|
|
39954
|
+
};
|
|
39955
|
+
}
|
|
39956
|
+
const vehicles = await _getLivePlatesBySite(String(site));
|
|
39957
|
+
const unique = (rows) => {
|
|
39958
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39959
|
+
return rows.filter((row) => {
|
|
39960
|
+
const key = normalisePlate(row.plateNumber);
|
|
39961
|
+
if (!key || seen.has(key))
|
|
39962
|
+
return false;
|
|
39963
|
+
seen.add(key);
|
|
39964
|
+
return true;
|
|
39965
|
+
});
|
|
39966
|
+
};
|
|
39967
|
+
const blocked = lists.block ? unique(vehicles.filter((vehicle) => listsHeldBy(vehicle).block)) : [];
|
|
39968
|
+
const allowed = lists.allow ? unique(vehicles.filter((vehicle) => listsHeldBy(vehicle).allow)) : [];
|
|
39969
|
+
const writes = [
|
|
39970
|
+
...blocked.map((vehicle) => ({
|
|
39971
|
+
plateNumber: String(vehicle.plateNumber),
|
|
39972
|
+
mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
|
|
39973
|
+
owner: String(vehicle.name ?? ""),
|
|
39974
|
+
...blockWindow(vehicle.end)
|
|
39975
|
+
})),
|
|
39976
|
+
...allowed.map((vehicle) => ({
|
|
39977
|
+
plateNumber: String(vehicle.plateNumber),
|
|
39978
|
+
mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
39979
|
+
owner: String(vehicle.name ?? ""),
|
|
39980
|
+
...cameraWindow(vehicle.start, vehicle.end)
|
|
39981
|
+
}))
|
|
39982
|
+
];
|
|
39983
|
+
const written = { allowed: 0, blocked: 0 };
|
|
39984
|
+
for (const write of writes) {
|
|
39985
|
+
try {
|
|
39986
|
+
await insertPlateOnCamera(target, write);
|
|
39987
|
+
} catch (error) {
|
|
39988
|
+
logCameraFailure("fill camera", target, error);
|
|
39989
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
39990
|
+
`Only ${written.allowed + written.blocked} of ${writes.length} plates could be added to ${cameraLabel(target)} before it stopped: ${anprWriteReason(error)}.`
|
|
39991
|
+
);
|
|
39992
|
+
}
|
|
39993
|
+
if (write.mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */)
|
|
39994
|
+
written.blocked++;
|
|
39995
|
+
else
|
|
39996
|
+
written.allowed++;
|
|
39997
|
+
}
|
|
39998
|
+
import_node_server_utils106.logger.info(
|
|
39999
|
+
`fillCameraLists wrote ${written.allowed} allowed and ${written.blocked} blocked plate(s) for site ${String(site)}`
|
|
40000
|
+
);
|
|
40001
|
+
return written;
|
|
40002
|
+
}
|
|
40003
|
+
async function newCameraPlateRows(site, direction) {
|
|
40004
|
+
const lists = listsFilledByDirectionChange("", direction);
|
|
40005
|
+
if (!lists.allow && !lists.block)
|
|
40006
|
+
return [];
|
|
40007
|
+
const vehicles = await _getLivePlatesBySite(String(site));
|
|
40008
|
+
const rowsFor = (list) => {
|
|
40009
|
+
const seen = /* @__PURE__ */ new Set();
|
|
40010
|
+
const rows = [];
|
|
40011
|
+
for (const vehicle of vehicles) {
|
|
40012
|
+
const held = listsHeldBy(vehicle);
|
|
40013
|
+
if (list === "blocked" ? !held.block : !held.allow)
|
|
40014
|
+
continue;
|
|
40015
|
+
const key = normalisePlate(vehicle.plateNumber);
|
|
40016
|
+
if (!key || seen.has(key))
|
|
40017
|
+
continue;
|
|
40018
|
+
seen.add(key);
|
|
40019
|
+
rows.push({
|
|
40020
|
+
id: `${list}:${String(vehicle._id)}`,
|
|
40021
|
+
vehicle: String(vehicle._id),
|
|
40022
|
+
list,
|
|
40023
|
+
plateNumber: String(vehicle.plateNumber ?? ""),
|
|
40024
|
+
name: String(vehicle.name ?? ""),
|
|
40025
|
+
block: String(vehicle.blockName ?? ""),
|
|
40026
|
+
level: String(vehicle.levelName ?? ""),
|
|
40027
|
+
unit: String(vehicle.unitName ?? ""),
|
|
40028
|
+
kind: list === "blocked" ? "Blocked" : vehicle.seasonPassType ? "Season pass" : "Whitelist"
|
|
40029
|
+
});
|
|
40030
|
+
}
|
|
40031
|
+
return rows;
|
|
40032
|
+
};
|
|
40033
|
+
return [
|
|
40034
|
+
...lists.block ? rowsFor("blocked") : [],
|
|
40035
|
+
...lists.allow ? rowsFor("allowed") : []
|
|
40036
|
+
];
|
|
40037
|
+
}
|
|
40038
|
+
async function writeVehicleToNewCamera(site, device, vehicleId, list, options = {}) {
|
|
40039
|
+
let siteId;
|
|
40040
|
+
let id;
|
|
40041
|
+
try {
|
|
40042
|
+
siteId = new import_mongodb72.ObjectId(String(site));
|
|
40043
|
+
id = new import_mongodb72.ObjectId(String(vehicleId));
|
|
40044
|
+
} catch {
|
|
40045
|
+
throw new import_node_server_utils106.BadRequestError("Invalid vehicle or site ID.");
|
|
40046
|
+
}
|
|
40047
|
+
const vehicle = await findOne({
|
|
40048
|
+
_id: id,
|
|
40049
|
+
site: siteId,
|
|
40050
|
+
status: "active" /* ACTIVE */
|
|
40051
|
+
});
|
|
40052
|
+
if (!vehicle)
|
|
40053
|
+
throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
|
|
40054
|
+
const held = listsHeldBy(vehicle);
|
|
40055
|
+
if (list === "blocked" ? !held.block : !held.allow) {
|
|
40056
|
+
throw new import_node_server_utils106.BadRequestError(
|
|
40057
|
+
"This vehicle no longer belongs on that list. Refresh and try again."
|
|
40058
|
+
);
|
|
40059
|
+
}
|
|
40060
|
+
const write = {
|
|
40061
|
+
plateNumber: String(vehicle.plateNumber ?? ""),
|
|
40062
|
+
mode: list === "blocked" ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
|
|
40063
|
+
owner: String(vehicle.name ?? ""),
|
|
40064
|
+
...list === "blocked" ? blockWindow(vehicle.end) : cameraWindow(vehicle.start, vehicle.end)
|
|
40065
|
+
};
|
|
40066
|
+
const target = {
|
|
40067
|
+
host: device.host,
|
|
40068
|
+
username: device.username,
|
|
40069
|
+
password: device.password,
|
|
40070
|
+
name: device.name,
|
|
40071
|
+
direction: device.direction,
|
|
40072
|
+
// Whose clock the season pass window is written on.
|
|
40073
|
+
site: siteId
|
|
40074
|
+
};
|
|
40075
|
+
try {
|
|
40076
|
+
if (options.onlyIfMissing)
|
|
40077
|
+
await ensurePlateOnCamera(target, write);
|
|
40078
|
+
else
|
|
40079
|
+
await insertPlateOnCamera(target, write);
|
|
40080
|
+
return { ok: true };
|
|
40081
|
+
} catch (error) {
|
|
40082
|
+
logCameraFailure("new camera plate", target, error);
|
|
40083
|
+
return {
|
|
40084
|
+
ok: false,
|
|
40085
|
+
reason: anprWriteReason(error),
|
|
40086
|
+
kind: classifyDahuaFailure(error?.message ?? error)
|
|
40087
|
+
};
|
|
40088
|
+
}
|
|
40089
|
+
}
|
|
39349
40090
|
return {
|
|
39350
40091
|
add,
|
|
39351
40092
|
deleteVehicle,
|
|
@@ -39355,7 +40096,10 @@ function useVehicleService() {
|
|
|
39355
40096
|
updateVehicleById,
|
|
39356
40097
|
bulkUpsertVehicles,
|
|
39357
40098
|
blocklistVehicles,
|
|
39358
|
-
removeFromBlocklist
|
|
40099
|
+
removeFromBlocklist,
|
|
40100
|
+
fillCameraLists,
|
|
40101
|
+
newCameraPlateRows,
|
|
40102
|
+
writeVehicleToNewCamera
|
|
39359
40103
|
};
|
|
39360
40104
|
}
|
|
39361
40105
|
|
|
@@ -43569,8 +44313,10 @@ function useVehicleController() {
|
|
|
43569
44313
|
}
|
|
43570
44314
|
const { _id, ...rest } = value;
|
|
43571
44315
|
await requireVehicleReach(req, _id);
|
|
43572
|
-
await _removeFromBlocklist(_id, rest);
|
|
43573
|
-
res.json({
|
|
44316
|
+
const message = await _removeFromBlocklist(_id, rest);
|
|
44317
|
+
res.json({
|
|
44318
|
+
message: message || "Successfully removed vehicle from blocklist."
|
|
44319
|
+
});
|
|
43574
44320
|
return;
|
|
43575
44321
|
} catch (error) {
|
|
43576
44322
|
import_node_server_utils120.logger.log({ level: "error", message: error.message });
|
|
@@ -43670,10 +44416,69 @@ function useSiteCameraService() {
|
|
|
43670
44416
|
session.endSession();
|
|
43671
44417
|
}
|
|
43672
44418
|
}
|
|
44419
|
+
async function clearPlateLists(site, camera, lists, action = "type-change") {
|
|
44420
|
+
let target;
|
|
44421
|
+
const id = typeof camera === "string" ? camera : "";
|
|
44422
|
+
if (typeof camera === "string") {
|
|
44423
|
+
const cameras = [];
|
|
44424
|
+
let page = 1;
|
|
44425
|
+
let pages = 1;
|
|
44426
|
+
const limit = 20;
|
|
44427
|
+
do {
|
|
44428
|
+
const siteCameraReq = await _getAllCameraWithPassword({
|
|
44429
|
+
site: String(site ?? ""),
|
|
44430
|
+
type: "anpr",
|
|
44431
|
+
page,
|
|
44432
|
+
limit
|
|
44433
|
+
});
|
|
44434
|
+
pages = siteCameraReq.pages || 1;
|
|
44435
|
+
cameras.push(...siteCameraReq.items);
|
|
44436
|
+
page++;
|
|
44437
|
+
} while (page <= pages);
|
|
44438
|
+
target = cameras.find((candidate) => String(candidate._id) === camera);
|
|
44439
|
+
if (!target)
|
|
44440
|
+
throw new import_node_server_utils121.NotFoundError("Camera not found.");
|
|
44441
|
+
} else {
|
|
44442
|
+
target = camera;
|
|
44443
|
+
}
|
|
44444
|
+
const label = cameraLabel(target);
|
|
44445
|
+
const notDone = action === "add" ? "The camera has NOT been added." : action === "sync" ? "No vehicles have been added to it." : "The camera type has NOT been changed.";
|
|
44446
|
+
const cleared = [];
|
|
44447
|
+
const steps = [
|
|
44448
|
+
...lists.allow ? [{ list: "allowed", mode: "TrafficRedList" /* TRAFFIC_REDLIST */ }] : [],
|
|
44449
|
+
...lists.block ? [{ list: "blocked", mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */ }] : []
|
|
44450
|
+
];
|
|
44451
|
+
for (const [index, step] of steps.entries()) {
|
|
44452
|
+
if (index > 0)
|
|
44453
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
44454
|
+
const outcome = await dahuaService.clearPlateList({
|
|
44455
|
+
host: target.host,
|
|
44456
|
+
username: target.username,
|
|
44457
|
+
password: target.password,
|
|
44458
|
+
mode: step.mode
|
|
44459
|
+
});
|
|
44460
|
+
if (!outcome?.ok) {
|
|
44461
|
+
import_node_server_utils121.logger.error(
|
|
44462
|
+
`clearPlateLists could not clear ${step.mode} on camera ${id || target.host}: ${outcome?.detail ?? "no outcome"}`
|
|
44463
|
+
);
|
|
44464
|
+
const listName = step.list === "allowed" ? "allowed list" : "blocklist";
|
|
44465
|
+
const reason = outcome?.reason || "the camera could not be reached";
|
|
44466
|
+
throw new import_node_server_utils121.BadRequestError(
|
|
44467
|
+
cleared.length ? `The allowed list on ${label} was cleared, but its blocklist could not be: ${reason}. ${notDone} Until it is sorted out, whitelist and season pass vehicles will not be let in through this camera. Please check the camera and try again.` : `The ${listName} on ${label} could not be cleared: ${reason}. Nothing on the camera has changed. ${notDone} Please check the camera and try again.`
|
|
44468
|
+
);
|
|
44469
|
+
}
|
|
44470
|
+
cleared.push(step.list);
|
|
44471
|
+
}
|
|
44472
|
+
import_node_server_utils121.logger.info(
|
|
44473
|
+
`clearPlateLists cleared ${cleared.join(" and ") || "nothing"} on camera ${id || target.host}`
|
|
44474
|
+
);
|
|
44475
|
+
return cleared;
|
|
44476
|
+
}
|
|
43673
44477
|
return {
|
|
43674
44478
|
add,
|
|
43675
44479
|
listenToCapturedPlateNumber,
|
|
43676
|
-
updateById
|
|
44480
|
+
updateById,
|
|
44481
|
+
clearPlateLists
|
|
43677
44482
|
};
|
|
43678
44483
|
}
|
|
43679
44484
|
|
|
@@ -43809,8 +44614,29 @@ function summariseBulkCameraPlan(results) {
|
|
|
43809
44614
|
}
|
|
43810
44615
|
|
|
43811
44616
|
// src/controllers/site-camera.controller.ts
|
|
44617
|
+
var CLEARED_ONLY_NOTE = " The camera's plate lists were cleared.";
|
|
44618
|
+
function filledNote(filled, cleared = true) {
|
|
44619
|
+
const parts = [
|
|
44620
|
+
...filled.allowed ? [`${filled.allowed} allowed`] : [],
|
|
44621
|
+
...filled.blocked ? [`${filled.blocked} blocked`] : []
|
|
44622
|
+
];
|
|
44623
|
+
const total = filled.allowed + filled.blocked;
|
|
44624
|
+
if (!total) {
|
|
44625
|
+
return cleared ? " The camera's plate lists were cleared; there were no vehicle plates to add." : " There were no vehicle plates to add to the camera.";
|
|
44626
|
+
}
|
|
44627
|
+
const added = `${parts.join(" and ")} ${total === 1 ? "plate was" : "plates were"} added`;
|
|
44628
|
+
return cleared ? ` The camera's plate lists were cleared and ${added} to it.` : ` ${added} to the camera.`;
|
|
44629
|
+
}
|
|
44630
|
+
function cameraListClearConfirmation(lists) {
|
|
44631
|
+
const what = lists.allow && lists.block ? "its allowed list and its blocklist" : lists.block ? "its blocklist" : "its allowed list";
|
|
44632
|
+
return `This change clears every plate on ${what}, on the camera itself - including plates added directly on the camera. Confirm the change to continue.`;
|
|
44633
|
+
}
|
|
43812
44634
|
function useSiteCameraController() {
|
|
43813
|
-
const {
|
|
44635
|
+
const {
|
|
44636
|
+
add: _add,
|
|
44637
|
+
updateById: _updateById,
|
|
44638
|
+
clearPlateLists: _clearPlateLists
|
|
44639
|
+
} = useSiteCameraService();
|
|
43814
44640
|
const {
|
|
43815
44641
|
add: _addOne,
|
|
43816
44642
|
activeHostsBySite: _activeHostsBySite,
|
|
@@ -43894,6 +44720,12 @@ function useSiteCameraController() {
|
|
|
43894
44720
|
return null;
|
|
43895
44721
|
}
|
|
43896
44722
|
async function add(req, res, next) {
|
|
44723
|
+
const clearCameraLists = req.body?.clearCameraLists === true;
|
|
44724
|
+
const skipPlateSync = req.body?.skipPlateSync === true;
|
|
44725
|
+
if (req.body && typeof req.body === "object") {
|
|
44726
|
+
delete req.body.clearCameraLists;
|
|
44727
|
+
delete req.body.skipPlateSync;
|
|
44728
|
+
}
|
|
43897
44729
|
const data = req.body;
|
|
43898
44730
|
const refusal = applyDerivedHost(data);
|
|
43899
44731
|
if (refusal) {
|
|
@@ -43912,6 +44744,52 @@ function useSiteCameraController() {
|
|
|
43912
44744
|
type: value.type,
|
|
43913
44745
|
notFound: "Site not found."
|
|
43914
44746
|
});
|
|
44747
|
+
const isActiveAnpr = !skipPlateSync && String(value.type ?? "").toLowerCase() === "anpr" && String(value.status ?? "active") === "active";
|
|
44748
|
+
const lists = isActiveAnpr ? listsFilledByDirectionChange("", value.direction) : { allow: false, block: false };
|
|
44749
|
+
const willClear = isActiveAnpr && clearCameraLists;
|
|
44750
|
+
let cleared = false;
|
|
44751
|
+
let filled = null;
|
|
44752
|
+
if (willClear || lists.allow || lists.block) {
|
|
44753
|
+
const taken = await _activeHostsBySite(value.site);
|
|
44754
|
+
if (taken.includes(value.host)) {
|
|
44755
|
+
throw new import_node_server_utils122.BadRequestError("ANPR already exist.");
|
|
44756
|
+
}
|
|
44757
|
+
const device = {
|
|
44758
|
+
host: value.host,
|
|
44759
|
+
username: value.username,
|
|
44760
|
+
password: value.password,
|
|
44761
|
+
name: value.name,
|
|
44762
|
+
direction: value.direction
|
|
44763
|
+
};
|
|
44764
|
+
const everything = { allow: true, block: true };
|
|
44765
|
+
if (willClear) {
|
|
44766
|
+
await _clearPlateLists(value.site, device, everything, "add");
|
|
44767
|
+
cleared = true;
|
|
44768
|
+
}
|
|
44769
|
+
if (lists.allow || lists.block) {
|
|
44770
|
+
try {
|
|
44771
|
+
filled = await useVehicleService().fillCameraLists(
|
|
44772
|
+
value.site,
|
|
44773
|
+
device,
|
|
44774
|
+
lists
|
|
44775
|
+
);
|
|
44776
|
+
} catch (error2) {
|
|
44777
|
+
let undone = null;
|
|
44778
|
+
if (willClear) {
|
|
44779
|
+
try {
|
|
44780
|
+
await _clearPlateLists(value.site, device, everything, "add");
|
|
44781
|
+
undone = true;
|
|
44782
|
+
} catch {
|
|
44783
|
+
undone = false;
|
|
44784
|
+
}
|
|
44785
|
+
}
|
|
44786
|
+
if (error2 && typeof error2 === "object") {
|
|
44787
|
+
error2.message = `${error2.message ?? ""} The camera has NOT been added` + (undone === true ? ` and the plates written to it were cleared again.` : undone === false ? `, and the plates written to it could NOT be cleared again - clear them on the camera by hand.` : `. The plates already written to it stay on the camera.`) + ` Please check the camera and try again.`;
|
|
44788
|
+
}
|
|
44789
|
+
throw error2;
|
|
44790
|
+
}
|
|
44791
|
+
}
|
|
44792
|
+
}
|
|
43915
44793
|
const vehicle = await _add(value);
|
|
43916
44794
|
await recordConsoleAction({
|
|
43917
44795
|
action: "camera.added" /* CAMERA_ADDED */,
|
|
@@ -43921,8 +44799,10 @@ function useSiteCameraController() {
|
|
|
43921
44799
|
after: pickAuditFields("camera.added" /* CAMERA_ADDED */, value)
|
|
43922
44800
|
});
|
|
43923
44801
|
res.status(201).json({
|
|
43924
|
-
message:
|
|
43925
|
-
data: { vehicle }
|
|
44802
|
+
message: `ANPR(s) added successfully.` + (filled ? filledNote(filled, cleared) : cleared ? CLEARED_ONLY_NOTE : ""),
|
|
44803
|
+
data: { vehicle },
|
|
44804
|
+
...filled ? { addedPlates: filled } : {},
|
|
44805
|
+
...cleared ? { clearedLists: ["allowed", "blocked"] } : {}
|
|
43926
44806
|
});
|
|
43927
44807
|
} catch (error2) {
|
|
43928
44808
|
next(error2);
|
|
@@ -44031,6 +44911,12 @@ function useSiteCameraController() {
|
|
|
44031
44911
|
}
|
|
44032
44912
|
}
|
|
44033
44913
|
async function updateById(req, res, next) {
|
|
44914
|
+
const confirmClearCameraLists = req.body?.confirmClearCameraLists === true;
|
|
44915
|
+
const skipPlateSync = req.body?.skipPlateSync === true;
|
|
44916
|
+
if (req.body && typeof req.body === "object") {
|
|
44917
|
+
delete req.body.confirmClearCameraLists;
|
|
44918
|
+
delete req.body.skipPlateSync;
|
|
44919
|
+
}
|
|
44034
44920
|
const value = req.body;
|
|
44035
44921
|
const id = req.params.id ?? "";
|
|
44036
44922
|
const refusal = applyDerivedHost(value);
|
|
@@ -44065,6 +44951,43 @@ function useSiteCameraController() {
|
|
|
44065
44951
|
throw error2;
|
|
44066
44952
|
}
|
|
44067
44953
|
const update = staffId ? staffUpdate(camera, value.status) : value;
|
|
44954
|
+
const lists = String(camera.type ?? "").toLowerCase() === "anpr" ? listsClearedByDirectionChange(camera.direction, update?.direction) : { allow: false, block: false };
|
|
44955
|
+
let clearedLists = null;
|
|
44956
|
+
if (lists.allow || lists.block) {
|
|
44957
|
+
if (!confirmClearCameraLists) {
|
|
44958
|
+
throw new import_node_server_utils122.BadRequestError(cameraListClearConfirmation(lists));
|
|
44959
|
+
}
|
|
44960
|
+
clearedLists = await _clearPlateLists(camera.site, id, lists);
|
|
44961
|
+
}
|
|
44962
|
+
const fill = !skipPlateSync && String(camera.type ?? "").toLowerCase() === "anpr" && String(update?.status ?? camera.status ?? "active") === "active" ? listsFilledByDirectionChange(camera.direction, update?.direction) : { allow: false, block: false };
|
|
44963
|
+
let filled = null;
|
|
44964
|
+
if (fill.allow || fill.block) {
|
|
44965
|
+
await _clearPlateLists(camera.site, id, fill);
|
|
44966
|
+
try {
|
|
44967
|
+
filled = await useVehicleService().fillCameraLists(
|
|
44968
|
+
camera.site,
|
|
44969
|
+
{
|
|
44970
|
+
id,
|
|
44971
|
+
host: update?.host,
|
|
44972
|
+
username: update?.username,
|
|
44973
|
+
password: update?.password
|
|
44974
|
+
},
|
|
44975
|
+
fill
|
|
44976
|
+
);
|
|
44977
|
+
} catch (error2) {
|
|
44978
|
+
let undone = false;
|
|
44979
|
+
try {
|
|
44980
|
+
await _clearPlateLists(camera.site, id, fill);
|
|
44981
|
+
undone = true;
|
|
44982
|
+
} catch {
|
|
44983
|
+
undone = false;
|
|
44984
|
+
}
|
|
44985
|
+
if (error2 && typeof error2 === "object") {
|
|
44986
|
+
error2.message = `${error2.message ?? ""} The camera type has NOT been changed` + (undone ? ` and the plates written to it were cleared again.` : `, and the plates written to it could NOT be cleared again - clear them on the camera by hand.`) + ` Please check the camera and try again.`;
|
|
44987
|
+
}
|
|
44988
|
+
throw error2;
|
|
44989
|
+
}
|
|
44990
|
+
}
|
|
44068
44991
|
const message = await _updateById(id, update);
|
|
44069
44992
|
if (staffId) {
|
|
44070
44993
|
await recordConsoleAction({
|
|
@@ -44086,8 +45009,11 @@ function useSiteCameraController() {
|
|
|
44086
45009
|
after: pickAuditFields("camera.updated" /* CAMERA_UPDATED */, update)
|
|
44087
45010
|
});
|
|
44088
45011
|
}
|
|
45012
|
+
const clearedNote = clearedLists === null ? "" : clearedLists.length === 2 ? " The camera's allowed list and blocklist were cleared." : clearedLists[0] === "blocked" ? " The camera's blocklist was cleared." : " The camera's allowed list was cleared.";
|
|
44089
45013
|
res.json({
|
|
44090
|
-
message
|
|
45014
|
+
message: `${message}${clearedNote}${filled ? filledNote(filled) : ""}`,
|
|
45015
|
+
...clearedLists === null ? {} : { clearedLists },
|
|
45016
|
+
...filled ? { addedPlates: filled } : {}
|
|
44091
45017
|
});
|
|
44092
45018
|
} catch (error2) {
|
|
44093
45019
|
next(error2);
|
|
@@ -44128,12 +45054,216 @@ function useSiteCameraController() {
|
|
|
44128
45054
|
next(error2);
|
|
44129
45055
|
}
|
|
44130
45056
|
}
|
|
45057
|
+
const schemaNewCameraDevice = {
|
|
45058
|
+
host: import_joi58.default.string().trim().required(),
|
|
45059
|
+
username: import_joi58.default.string().required(),
|
|
45060
|
+
password: import_joi58.default.string().required(),
|
|
45061
|
+
name: import_joi58.default.string().optional().allow("", null),
|
|
45062
|
+
direction: import_joi58.default.string().required().valid("entry", "exit", "both", "visitors", "residents")
|
|
45063
|
+
};
|
|
45064
|
+
async function authorizeNewCamera(req, siteId, host) {
|
|
45065
|
+
await authorizeWrite({
|
|
45066
|
+
req,
|
|
45067
|
+
site: siteId,
|
|
45068
|
+
type: "anpr",
|
|
45069
|
+
notFound: "Site not found."
|
|
45070
|
+
});
|
|
45071
|
+
const taken = await _activeHostsBySite(siteId);
|
|
45072
|
+
if (taken.includes(host)) {
|
|
45073
|
+
throw new import_node_server_utils122.BadRequestError("ANPR already exist.");
|
|
45074
|
+
}
|
|
45075
|
+
}
|
|
45076
|
+
async function newCameraPlates(req, res, next) {
|
|
45077
|
+
const { error, value } = import_joi58.default.object({
|
|
45078
|
+
siteId: import_joi58.default.string().hex().length(24).required(),
|
|
45079
|
+
host: import_joi58.default.string().trim().required(),
|
|
45080
|
+
direction: schemaNewCameraDevice.direction
|
|
45081
|
+
}).validate({
|
|
45082
|
+
siteId: req.params.siteId,
|
|
45083
|
+
host: req.query?.host,
|
|
45084
|
+
direction: req.query?.direction
|
|
45085
|
+
});
|
|
45086
|
+
if (error) {
|
|
45087
|
+
next(new import_node_server_utils122.BadRequestError(error.message));
|
|
45088
|
+
return;
|
|
45089
|
+
}
|
|
45090
|
+
try {
|
|
45091
|
+
await authorizeNewCamera(req, value.siteId, value.host);
|
|
45092
|
+
const items = await useVehicleService().newCameraPlateRows(
|
|
45093
|
+
value.siteId,
|
|
45094
|
+
value.direction
|
|
45095
|
+
);
|
|
45096
|
+
res.json({ items });
|
|
45097
|
+
} catch (error2) {
|
|
45098
|
+
next(error2);
|
|
45099
|
+
}
|
|
45100
|
+
}
|
|
45101
|
+
async function clearNewCamera(req, res, next) {
|
|
45102
|
+
const { error, value } = import_joi58.default.object({
|
|
45103
|
+
siteId: import_joi58.default.string().hex().length(24).required(),
|
|
45104
|
+
...schemaNewCameraDevice
|
|
45105
|
+
}).validate({ siteId: req.params.siteId, ...req.body ?? {} });
|
|
45106
|
+
if (error) {
|
|
45107
|
+
next(new import_node_server_utils122.BadRequestError(error.message));
|
|
45108
|
+
return;
|
|
45109
|
+
}
|
|
45110
|
+
try {
|
|
45111
|
+
await authorizeNewCamera(req, value.siteId, value.host);
|
|
45112
|
+
const { siteId, ...device } = value;
|
|
45113
|
+
const cleared = await _clearPlateLists(
|
|
45114
|
+
siteId,
|
|
45115
|
+
device,
|
|
45116
|
+
{ allow: true, block: true },
|
|
45117
|
+
"add"
|
|
45118
|
+
);
|
|
45119
|
+
res.json({
|
|
45120
|
+
message: "The camera's allowed list and blocklist were cleared.",
|
|
45121
|
+
clearedLists: cleared
|
|
45122
|
+
});
|
|
45123
|
+
} catch (error2) {
|
|
45124
|
+
next(error2);
|
|
45125
|
+
}
|
|
45126
|
+
}
|
|
45127
|
+
async function addNewCameraPlate(req, res, next) {
|
|
45128
|
+
const { error, value } = import_joi58.default.object({
|
|
45129
|
+
siteId: import_joi58.default.string().hex().length(24).required(),
|
|
45130
|
+
...schemaNewCameraDevice,
|
|
45131
|
+
vehicle: import_joi58.default.string().hex().length(24).required(),
|
|
45132
|
+
list: import_joi58.default.string().valid("allowed", "blocked").required(),
|
|
45133
|
+
onlyIfMissing: import_joi58.default.boolean().optional().default(false)
|
|
45134
|
+
}).validate({ siteId: req.params.siteId, ...req.body ?? {} });
|
|
45135
|
+
if (error) {
|
|
45136
|
+
next(new import_node_server_utils122.BadRequestError(error.message));
|
|
45137
|
+
return;
|
|
45138
|
+
}
|
|
45139
|
+
try {
|
|
45140
|
+
await authorizeNewCamera(req, value.siteId, value.host);
|
|
45141
|
+
const { siteId, vehicle, list, onlyIfMissing, ...device } = value;
|
|
45142
|
+
const outcome = await useVehicleService().writeVehicleToNewCamera(
|
|
45143
|
+
siteId,
|
|
45144
|
+
device,
|
|
45145
|
+
vehicle,
|
|
45146
|
+
list,
|
|
45147
|
+
{ onlyIfMissing }
|
|
45148
|
+
);
|
|
45149
|
+
res.json(outcome);
|
|
45150
|
+
} catch (error2) {
|
|
45151
|
+
next(error2);
|
|
45152
|
+
}
|
|
45153
|
+
}
|
|
45154
|
+
async function loadForSync(req, id) {
|
|
45155
|
+
if (!import_mongodb78.ObjectId.isValid(id))
|
|
45156
|
+
throw new import_node_server_utils122.NotFoundError("Camera not found.");
|
|
45157
|
+
const camera = await _findOne(
|
|
45158
|
+
{ _id: new import_mongodb78.ObjectId(id) },
|
|
45159
|
+
{
|
|
45160
|
+
site: 1,
|
|
45161
|
+
type: 1,
|
|
45162
|
+
host: 1,
|
|
45163
|
+
username: 1,
|
|
45164
|
+
password: 1,
|
|
45165
|
+
name: 1,
|
|
45166
|
+
direction: 1,
|
|
45167
|
+
status: 1
|
|
45168
|
+
}
|
|
45169
|
+
);
|
|
45170
|
+
if (!camera)
|
|
45171
|
+
throw new import_node_server_utils122.NotFoundError("Camera not found.");
|
|
45172
|
+
await authorizeWrite({
|
|
45173
|
+
req,
|
|
45174
|
+
site: camera.site,
|
|
45175
|
+
type: camera.type,
|
|
45176
|
+
notFound: "Camera not found."
|
|
45177
|
+
});
|
|
45178
|
+
if (String(camera.type ?? "").toLowerCase() !== "anpr") {
|
|
45179
|
+
throw new import_node_server_utils122.BadRequestError("Only ANPR cameras hold vehicle plates.");
|
|
45180
|
+
}
|
|
45181
|
+
if (String(camera.status ?? "active") !== "active") {
|
|
45182
|
+
throw new import_node_server_utils122.BadRequestError(
|
|
45183
|
+
"This camera is inactive. Switch it on before adding vehicles to it."
|
|
45184
|
+
);
|
|
45185
|
+
}
|
|
45186
|
+
const lists = listsFilledByDirectionChange("", camera.direction);
|
|
45187
|
+
if (!lists.allow && !lists.block) {
|
|
45188
|
+
throw new import_node_server_utils122.BadRequestError("Exit cameras receive no vehicle plates.");
|
|
45189
|
+
}
|
|
45190
|
+
return {
|
|
45191
|
+
site: String(camera.site ?? ""),
|
|
45192
|
+
device: {
|
|
45193
|
+
host: String(camera.host ?? ""),
|
|
45194
|
+
username: String(camera.username ?? ""),
|
|
45195
|
+
password: String(camera.password ?? ""),
|
|
45196
|
+
name: camera.name ?? "",
|
|
45197
|
+
direction: String(camera.direction ?? "")
|
|
45198
|
+
}
|
|
45199
|
+
};
|
|
45200
|
+
}
|
|
45201
|
+
async function cameraPlates(req, res, next) {
|
|
45202
|
+
try {
|
|
45203
|
+
const { site, device } = await loadForSync(req, String(req.params.id));
|
|
45204
|
+
const items = await useVehicleService().newCameraPlateRows(
|
|
45205
|
+
site,
|
|
45206
|
+
device.direction
|
|
45207
|
+
);
|
|
45208
|
+
res.json({ items });
|
|
45209
|
+
} catch (error) {
|
|
45210
|
+
next(error);
|
|
45211
|
+
}
|
|
45212
|
+
}
|
|
45213
|
+
async function clearCamera(req, res, next) {
|
|
45214
|
+
try {
|
|
45215
|
+
const { site, device } = await loadForSync(req, String(req.params.id));
|
|
45216
|
+
const cleared = await _clearPlateLists(
|
|
45217
|
+
site,
|
|
45218
|
+
device,
|
|
45219
|
+
{ allow: true, block: true },
|
|
45220
|
+
"sync"
|
|
45221
|
+
);
|
|
45222
|
+
res.json({
|
|
45223
|
+
message: "The camera's allowed list and blocklist were cleared.",
|
|
45224
|
+
clearedLists: cleared
|
|
45225
|
+
});
|
|
45226
|
+
} catch (error) {
|
|
45227
|
+
next(error);
|
|
45228
|
+
}
|
|
45229
|
+
}
|
|
45230
|
+
async function addCameraPlate(req, res, next) {
|
|
45231
|
+
const { error, value } = import_joi58.default.object({
|
|
45232
|
+
vehicle: import_joi58.default.string().hex().length(24).required(),
|
|
45233
|
+
list: import_joi58.default.string().valid("allowed", "blocked").required(),
|
|
45234
|
+
onlyIfMissing: import_joi58.default.boolean().optional().default(true)
|
|
45235
|
+
}).validate(req.body ?? {});
|
|
45236
|
+
if (error) {
|
|
45237
|
+
next(new import_node_server_utils122.BadRequestError(error.message));
|
|
45238
|
+
return;
|
|
45239
|
+
}
|
|
45240
|
+
try {
|
|
45241
|
+
const { site, device } = await loadForSync(req, String(req.params.id));
|
|
45242
|
+
const outcome = await useVehicleService().writeVehicleToNewCamera(
|
|
45243
|
+
site,
|
|
45244
|
+
device,
|
|
45245
|
+
value.vehicle,
|
|
45246
|
+
value.list,
|
|
45247
|
+
// Absent means look first: a saved camera may already hold the plate.
|
|
45248
|
+
{ onlyIfMissing: value.onlyIfMissing !== false }
|
|
45249
|
+
);
|
|
45250
|
+
res.json(outcome);
|
|
45251
|
+
} catch (error2) {
|
|
45252
|
+
next(error2);
|
|
45253
|
+
}
|
|
45254
|
+
}
|
|
44131
45255
|
return {
|
|
44132
45256
|
add,
|
|
44133
45257
|
addBulk,
|
|
44134
45258
|
getAll,
|
|
44135
45259
|
updateById,
|
|
44136
|
-
deleteById
|
|
45260
|
+
deleteById,
|
|
45261
|
+
newCameraPlates,
|
|
45262
|
+
clearNewCamera,
|
|
45263
|
+
addNewCameraPlate,
|
|
45264
|
+
cameraPlates,
|
|
45265
|
+
clearCamera,
|
|
45266
|
+
addCameraPlate
|
|
44137
45267
|
};
|
|
44138
45268
|
}
|
|
44139
45269
|
|
|
@@ -47730,7 +48860,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
|
47730
48860
|
]);
|
|
47731
48861
|
|
|
47732
48862
|
// src/services/hid-amico.service.ts
|
|
47733
|
-
var
|
|
48863
|
+
var import_crypto4 = __toESM(require("crypto"));
|
|
47734
48864
|
var import_axios = __toESM(require("axios"));
|
|
47735
48865
|
var import_node_server_utils140 = require("@7365admin1/node-server-utils");
|
|
47736
48866
|
|
|
@@ -49685,7 +50815,7 @@ function stringifyHidJson(value) {
|
|
|
49685
50815
|
}
|
|
49686
50816
|
|
|
49687
50817
|
// src/utils/hid-secret.util.ts
|
|
49688
|
-
var
|
|
50818
|
+
var import_crypto3 = __toESM(require("crypto"));
|
|
49689
50819
|
var HID_SECRET_ENV = "HID_AMICO_SECRET";
|
|
49690
50820
|
var HID_LEGACY_SECRET_ENV = "ACCESS_TOKEN_SECRET";
|
|
49691
50821
|
var HID_SECRET_MISSING_FOR_WRITE = `${HID_SECRET_ENV} is not configured, so a new HID credential cannot be encrypted. Set ${HID_SECRET_ENV} on this service and try again. It is deliberately NOT falling back to ${HID_LEGACY_SECRET_ENV}: that key is rotated independently, and anything encrypted with it becomes permanently unreadable the moment it is.`;
|
|
@@ -49706,7 +50836,7 @@ function resolveHidSecretKey(purpose, env2 = process.env) {
|
|
|
49706
50836
|
return { key: deriveKey(legacy), usedLegacyFallback: true };
|
|
49707
50837
|
}
|
|
49708
50838
|
function deriveKey(secret) {
|
|
49709
|
-
return
|
|
50839
|
+
return import_crypto3.default.createHash("sha256").update(secret).digest();
|
|
49710
50840
|
}
|
|
49711
50841
|
|
|
49712
50842
|
// src/services/hid-amico.service.ts
|
|
@@ -49910,8 +51040,8 @@ function getPublicMonitorConfiguration(monitor, callbackToken) {
|
|
|
49910
51040
|
};
|
|
49911
51041
|
}
|
|
49912
51042
|
function encryptSecret(value) {
|
|
49913
|
-
const iv =
|
|
49914
|
-
const cipher =
|
|
51043
|
+
const iv = import_crypto4.default.randomBytes(12);
|
|
51044
|
+
const cipher = import_crypto4.default.createCipheriv("aes-256-gcm", getSecretKey("encrypt"), iv);
|
|
49915
51045
|
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
49916
51046
|
const tag = cipher.getAuthTag();
|
|
49917
51047
|
return [PASSWORD_PREFIX, iv.toString("hex"), tag.toString("hex"), encrypted.toString("hex")].join(":");
|
|
@@ -49923,7 +51053,7 @@ function decryptSecret(value) {
|
|
|
49923
51053
|
const key = getSecretKey("decrypt");
|
|
49924
51054
|
try {
|
|
49925
51055
|
const [, ivHex, tagHex, encryptedHex] = value.split(":");
|
|
49926
|
-
const decipher =
|
|
51056
|
+
const decipher = import_crypto4.default.createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
49927
51057
|
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
49928
51058
|
return Buffer.concat([
|
|
49929
51059
|
decipher.update(Buffer.from(encryptedHex, "hex")),
|
|
@@ -49963,7 +51093,7 @@ function normalizeHidReaderBaseUrl(value) {
|
|
|
49963
51093
|
return endpoint.origin;
|
|
49964
51094
|
}
|
|
49965
51095
|
function createStableHidId(seed) {
|
|
49966
|
-
const bytes =
|
|
51096
|
+
const bytes = import_crypto4.default.createHash("sha256").update(seed).digest().subarray(0, 6);
|
|
49967
51097
|
return Number.parseInt(bytes.toString("hex"), 16) || 1;
|
|
49968
51098
|
}
|
|
49969
51099
|
function isUnknownRecord2(value) {
|
|
@@ -50038,7 +51168,7 @@ function resolveProfileIdentityType(memberType, userType) {
|
|
|
50038
51168
|
var VISITOR_QR_FORMAT = "0";
|
|
50039
51169
|
function createVisitorQrCredential(visitorId, readerId, qrValue) {
|
|
50040
51170
|
const credentialId = createStableHidId(`visitor-qr:${readerId}:${visitorId}`);
|
|
50041
|
-
const token = qrValue || `VIS${
|
|
51171
|
+
const token = qrValue || `VIS${import_crypto4.default.randomBytes(12).toString("hex").toUpperCase()}`;
|
|
50042
51172
|
return { credentialId, object: "qrcodes", value: token, qrValue: token };
|
|
50043
51173
|
}
|
|
50044
51174
|
async function configureVisitorQrReader(client) {
|
|
@@ -50542,7 +51672,7 @@ function getSipProvisioningConfig() {
|
|
|
50542
51672
|
return { provisionerUrl, provisionerToken, websocketUrl, domain };
|
|
50543
51673
|
}
|
|
50544
51674
|
function createSipPassword() {
|
|
50545
|
-
return
|
|
51675
|
+
return import_crypto4.default.randomBytes(12).toString("base64url");
|
|
50546
51676
|
}
|
|
50547
51677
|
function sipAccountResponse(account) {
|
|
50548
51678
|
const { websocketUrl, domain } = getSipProvisioningConfig();
|
|
@@ -53222,7 +54352,7 @@ function useHidAmicoService() {
|
|
|
53222
54352
|
assertReaderCardProfile(reader, cardType);
|
|
53223
54353
|
const hidUserId = Number(hidUserIdValue);
|
|
53224
54354
|
const client = new HidAmicoClient(reader);
|
|
53225
|
-
const lockOwner =
|
|
54355
|
+
const lockOwner = import_crypto4.default.randomUUID();
|
|
53226
54356
|
const acquired = await repo.acquireEnrollmentLock(readerId, lockOwner, timeoutSeconds + 30);
|
|
53227
54357
|
if (!acquired) {
|
|
53228
54358
|
throw new import_node_server_utils140.BadRequestError("Another physical HID card enrollment is already running on this reader.");
|
|
@@ -55504,17 +56634,17 @@ var import_fs5 = __toESM(require("fs"));
|
|
|
55504
56634
|
var import_path2 = __toESM(require("path"));
|
|
55505
56635
|
var import_axios2 = __toESM(require("axios"));
|
|
55506
56636
|
var import_xml2js = require("xml2js");
|
|
55507
|
-
var
|
|
56637
|
+
var import_crypto5 = __toESM(require("crypto"));
|
|
55508
56638
|
var ALGORITHM = "aes-256-gcm";
|
|
55509
|
-
var SECRET_KEY =
|
|
55510
|
-
new Uint8Array(
|
|
56639
|
+
var SECRET_KEY = import_crypto5.default.createSecretKey(
|
|
56640
|
+
new Uint8Array(import_crypto5.default.createHash("sha256").update("acm-secret-key").digest())
|
|
55511
56641
|
);
|
|
55512
56642
|
function decryptAcmUrl(encryptedText) {
|
|
55513
56643
|
const [ivB64, authTagB64, encryptedB64] = encryptedText.split(":");
|
|
55514
56644
|
const iv = Buffer.from(ivB64, "base64");
|
|
55515
56645
|
const authTag = Buffer.from(authTagB64, "base64");
|
|
55516
56646
|
const encrypted = Buffer.from(encryptedB64, "base64");
|
|
55517
|
-
const decipher =
|
|
56647
|
+
const decipher = import_crypto5.default.createDecipheriv(
|
|
55518
56648
|
ALGORITHM,
|
|
55519
56649
|
SECRET_KEY,
|
|
55520
56650
|
new Uint8Array(iv)
|
|
@@ -55657,10 +56787,10 @@ async function getTransactions(index, url) {
|
|
|
55657
56787
|
}
|
|
55658
56788
|
function encryptPayload(payloadHex) {
|
|
55659
56789
|
const buffer = Buffer.from(payloadHex, "ascii");
|
|
55660
|
-
const encrypted =
|
|
56790
|
+
const encrypted = import_crypto5.default.publicEncrypt(
|
|
55661
56791
|
{
|
|
55662
56792
|
key: "testing",
|
|
55663
|
-
padding:
|
|
56793
|
+
padding: import_crypto5.default.constants.RSA_PKCS1_PADDING
|
|
55664
56794
|
},
|
|
55665
56795
|
buffer
|
|
55666
56796
|
);
|
|
@@ -55670,7 +56800,7 @@ function toHex(num, length) {
|
|
|
55670
56800
|
return num.toString(16).padStart(length * 2, "0");
|
|
55671
56801
|
}
|
|
55672
56802
|
function buildPayload(cardNumber, validityMinutes) {
|
|
55673
|
-
const randomHex =
|
|
56803
|
+
const randomHex = import_crypto5.default.randomBytes(2).toString("hex");
|
|
55674
56804
|
const cardHex = toHex(cardNumber, 4);
|
|
55675
56805
|
const epochHex = toHex(Math.floor(Date.now() / 1e3), 4);
|
|
55676
56806
|
const validityHex = toHex(validityMinutes, 2);
|
|
@@ -60613,6 +61743,21 @@ var import_joi72 = __toESM(require("joi"));
|
|
|
60613
61743
|
var import_node_server_utils148 = require("@7365admin1/node-server-utils");
|
|
60614
61744
|
var import_node_server_utils149 = require("@7365admin1/node-server-utils");
|
|
60615
61745
|
|
|
61746
|
+
// src/utils/people-create-gate.util.ts
|
|
61747
|
+
var PEOPLE_CREATE_REACH_MODE_DEFAULT = "log";
|
|
61748
|
+
function peopleCreateReachMode(env2 = process.env) {
|
|
61749
|
+
return gateMode(
|
|
61750
|
+
env2.PEOPLE_CREATE_REACH_MODE,
|
|
61751
|
+
["log", "enforce", "off"],
|
|
61752
|
+
PEOPLE_CREATE_REACH_MODE_DEFAULT
|
|
61753
|
+
);
|
|
61754
|
+
}
|
|
61755
|
+
function peopleCreateVerdict(params) {
|
|
61756
|
+
if (params.mode === "off" || params.reachable)
|
|
61757
|
+
return { refuse: false, record: false };
|
|
61758
|
+
return { refuse: params.mode === "enforce", record: true };
|
|
61759
|
+
}
|
|
61760
|
+
|
|
60616
61761
|
// src/utils/self-signup-files.util.ts
|
|
60617
61762
|
function dropUnsentFiles(body) {
|
|
60618
61763
|
if (!Array.isArray(body?.files))
|
|
@@ -60721,6 +61866,41 @@ function usePersonController() {
|
|
|
60721
61866
|
next(new import_node_server_utils148.BadRequestError(messages));
|
|
60722
61867
|
return;
|
|
60723
61868
|
}
|
|
61869
|
+
try {
|
|
61870
|
+
const mode = peopleCreateReachMode();
|
|
61871
|
+
if (mode !== "off") {
|
|
61872
|
+
const actor = await resolveInviteActor(callerId(req));
|
|
61873
|
+
let reachable = true;
|
|
61874
|
+
if (!staffMayBypass(actor)) {
|
|
61875
|
+
const site = payload?.site ? String(payload.site) : "";
|
|
61876
|
+
const org = payload?.org ? String(payload.org) : "";
|
|
61877
|
+
if (site) {
|
|
61878
|
+
reachable = await useCameraViewService().entitleSite({ siteId: site, userId: actor.id }).then(
|
|
61879
|
+
() => true,
|
|
61880
|
+
() => false
|
|
61881
|
+
);
|
|
61882
|
+
} else if (org) {
|
|
61883
|
+
reachable = actor.orgIds.includes(org);
|
|
61884
|
+
}
|
|
61885
|
+
}
|
|
61886
|
+
const verdict = peopleCreateVerdict({ mode, reachable });
|
|
61887
|
+
if (verdict.record) {
|
|
61888
|
+
import_node_server_utils148.logger.log({
|
|
61889
|
+
level: "info",
|
|
61890
|
+
message: `[people-create] ${verdict.refuse ? "refused" : "would-refuse"} caller=${actor.id} site=${payload?.site ?? ""} org=${payload?.org ?? ""} mode=${mode}`
|
|
61891
|
+
});
|
|
61892
|
+
}
|
|
61893
|
+
if (verdict.refuse) {
|
|
61894
|
+
next(new import_node_server_utils149.UnauthorizedError("Not authorized."));
|
|
61895
|
+
return;
|
|
61896
|
+
}
|
|
61897
|
+
}
|
|
61898
|
+
} catch (gateError) {
|
|
61899
|
+
import_node_server_utils148.logger.log({
|
|
61900
|
+
level: "warn",
|
|
61901
|
+
message: `[people-create] reach check failed open: ${gateError?.message}`
|
|
61902
|
+
});
|
|
61903
|
+
}
|
|
60724
61904
|
try {
|
|
60725
61905
|
const data = await _add(payload);
|
|
60726
61906
|
res.status(201).json({ message: data });
|
|
@@ -85931,6 +87111,52 @@ var isWithinHour = (alertTime, timeNow) => {
|
|
|
85931
87111
|
var import_node_server_utils264 = require("@7365admin1/node-server-utils");
|
|
85932
87112
|
var import_mongodb162 = require("mongodb");
|
|
85933
87113
|
var import_moment_timezone11 = __toESM(require("moment-timezone"));
|
|
87114
|
+
|
|
87115
|
+
// src/utils/socket-session.util.ts
|
|
87116
|
+
var MANPOWER_LIVE_NAMESPACE = "/manpower-live";
|
|
87117
|
+
function sidFromHandshake(handshake) {
|
|
87118
|
+
const cookie = String(handshake?.headers?.cookie ?? "");
|
|
87119
|
+
const fromCookie = /(?:^|;\s*)sid=([^;]*)/.exec(cookie);
|
|
87120
|
+
if (fromCookie) {
|
|
87121
|
+
const value = decodeURIComponent(fromCookie[1]).trim();
|
|
87122
|
+
if (value)
|
|
87123
|
+
return value;
|
|
87124
|
+
}
|
|
87125
|
+
for (const key of ["sid", "token"]) {
|
|
87126
|
+
const value = handshake?.auth?.[key];
|
|
87127
|
+
if (typeof value === "string" && value.trim())
|
|
87128
|
+
return value.trim();
|
|
87129
|
+
}
|
|
87130
|
+
const header = handshake?.headers?.authorization;
|
|
87131
|
+
if (typeof header === "string" && header.trim()) {
|
|
87132
|
+
const value = header.trim();
|
|
87133
|
+
return value.toLowerCase().startsWith("bearer ") ? value.slice("bearer ".length).trim() : value;
|
|
87134
|
+
}
|
|
87135
|
+
return "";
|
|
87136
|
+
}
|
|
87137
|
+
function userIdOfSession(session) {
|
|
87138
|
+
if (!session || typeof session !== "object")
|
|
87139
|
+
return "";
|
|
87140
|
+
const record = session;
|
|
87141
|
+
return String(record.user ?? record._id ?? "").trim();
|
|
87142
|
+
}
|
|
87143
|
+
async function authoriseSocketSession(handshake, readSession) {
|
|
87144
|
+
const sid = sidFromHandshake(handshake);
|
|
87145
|
+
if (!sid)
|
|
87146
|
+
throw new Error("Unauthorized");
|
|
87147
|
+
let session;
|
|
87148
|
+
try {
|
|
87149
|
+
session = await readSession(sid);
|
|
87150
|
+
} catch {
|
|
87151
|
+
throw new Error("Unauthorized");
|
|
87152
|
+
}
|
|
87153
|
+
const userId = userIdOfSession(session);
|
|
87154
|
+
if (!userId)
|
|
87155
|
+
throw new Error("Unauthorized");
|
|
87156
|
+
return userId;
|
|
87157
|
+
}
|
|
87158
|
+
|
|
87159
|
+
// src/events/manpower.event.ts
|
|
85934
87160
|
async function manpowerEvents(io) {
|
|
85935
87161
|
let intervalId = null;
|
|
85936
87162
|
let activeConnections = 0;
|
|
@@ -85991,8 +87217,7 @@ async function manpowerEvents(io) {
|
|
|
85991
87217
|
newStatus = "Over";
|
|
85992
87218
|
else if (actual < expected)
|
|
85993
87219
|
newStatus = "Under";
|
|
85994
|
-
const
|
|
85995
|
-
io.of(namespace2).emit("manpower-status-update", {
|
|
87220
|
+
const update = {
|
|
85996
87221
|
siteId: doc.siteId,
|
|
85997
87222
|
siteName: doc.siteName,
|
|
85998
87223
|
shift: shift.key,
|
|
@@ -86000,7 +87225,9 @@ async function manpowerEvents(io) {
|
|
|
86000
87225
|
actual,
|
|
86001
87226
|
expected,
|
|
86002
87227
|
selectedDateSGT
|
|
86003
|
-
}
|
|
87228
|
+
};
|
|
87229
|
+
io.of("/manpower").emit("manpower-status-update", update);
|
|
87230
|
+
io.of(MANPOWER_LIVE_NAMESPACE).emit("manpower-status-update", update);
|
|
86004
87231
|
}
|
|
86005
87232
|
}
|
|
86006
87233
|
};
|
|
@@ -86020,7 +87247,7 @@ async function manpowerEvents(io) {
|
|
|
86020
87247
|
console.log(" Interval stopped.");
|
|
86021
87248
|
}
|
|
86022
87249
|
};
|
|
86023
|
-
|
|
87250
|
+
const onConnection = (socket) => {
|
|
86024
87251
|
activeConnections += 1;
|
|
86025
87252
|
const date = socket.handshake.query.date;
|
|
86026
87253
|
if (activeConnections === 1) {
|
|
@@ -86035,7 +87262,26 @@ async function manpowerEvents(io) {
|
|
|
86035
87262
|
stopInterval();
|
|
86036
87263
|
}
|
|
86037
87264
|
});
|
|
87265
|
+
};
|
|
87266
|
+
io.of("/manpower").on("connection", onConnection);
|
|
87267
|
+
const live = io.of(MANPOWER_LIVE_NAMESPACE);
|
|
87268
|
+
live.use(async (socket, next) => {
|
|
87269
|
+
try {
|
|
87270
|
+
const { getCache } = (0, import_node_server_utils264.useCache)("sessions");
|
|
87271
|
+
socket.data.userId = await authoriseSocketSession(
|
|
87272
|
+
socket.handshake,
|
|
87273
|
+
(sid) => getCache(`sid:${sid}`)
|
|
87274
|
+
);
|
|
87275
|
+
next();
|
|
87276
|
+
} catch (error) {
|
|
87277
|
+
import_node_server_utils264.logger.log({
|
|
87278
|
+
level: "debug",
|
|
87279
|
+
message: `[manpower] refused a socket: ${error?.message}`
|
|
87280
|
+
});
|
|
87281
|
+
next(new Error("Unauthorized"));
|
|
87282
|
+
}
|
|
86038
87283
|
});
|
|
87284
|
+
live.on("connection", onConnection);
|
|
86039
87285
|
}
|
|
86040
87286
|
|
|
86041
87287
|
// src/services/reddot-payment.service.ts
|
|
@@ -86919,7 +88165,7 @@ function useRedDotPaymentController() {
|
|
|
86919
88165
|
|
|
86920
88166
|
// src/services/verification-v2.service.ts
|
|
86921
88167
|
var import_node_server_utils271 = require("@7365admin1/node-server-utils");
|
|
86922
|
-
var
|
|
88168
|
+
var import_crypto6 = __toESM(require("crypto"));
|
|
86923
88169
|
var import_joi142 = __toESM(require("joi"));
|
|
86924
88170
|
|
|
86925
88171
|
// src/services/auth-v2.service.ts
|
|
@@ -87617,7 +88863,7 @@ function useVerificationServiceV2() {
|
|
|
87617
88863
|
}
|
|
87618
88864
|
function generateVerificationCode(length = 8) {
|
|
87619
88865
|
const chars = "0123456789";
|
|
87620
|
-
const bytes =
|
|
88866
|
+
const bytes = import_crypto6.default.randomBytes(length);
|
|
87621
88867
|
let result = "";
|
|
87622
88868
|
for (let i = 0; i < length; i++) {
|
|
87623
88869
|
result += chars[bytes[i] % chars.length];
|
|
@@ -94810,6 +96056,8 @@ async function moduleDeniedForCaller({
|
|
|
94810
96056
|
if (!user || !siteId)
|
|
94811
96057
|
return ALLOW;
|
|
94812
96058
|
const siteDoc = typeof site === "string" ? await data.site(siteId).catch(() => null) : site ?? null;
|
|
96059
|
+
if (!await siteHasEnforcedList(siteDoc, data))
|
|
96060
|
+
return ALLOW;
|
|
94813
96061
|
const gate = await gateAtSite(await data.membershipsOf([user]), siteDoc, siteId, data);
|
|
94814
96062
|
if (gate.exempt)
|
|
94815
96063
|
return ALLOW;
|
|
@@ -95448,6 +96696,7 @@ function usePersonalEmergencyChainController() {
|
|
|
95448
96696
|
cameraGrant,
|
|
95449
96697
|
cameraHealthClaim,
|
|
95450
96698
|
cameraHealthSummary,
|
|
96699
|
+
cameraListClearConfirmation,
|
|
95451
96700
|
cameraManagePermissions,
|
|
95452
96701
|
cameraProbeCacheKey,
|
|
95453
96702
|
cameraTransports,
|